From a13fb3cf00018dba45b8ae83ca2466d92cd79593 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 22 May 2026 18:22:22 +0200 Subject: [PATCH] fix(ssh): verify mux readiness before reusing socket Use ssh -O check in the first-use mux lock flow so commands only reuse a multiplexed socket after the control master is actually ready. --- app/Helpers/SshMultiplexingHelper.php | 33 ++++++++++++++++------- tests/Feature/SshMultiplexingLockTest.php | 8 +++--- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php index 6984dac4b..db139d110 100644 --- a/app/Helpers/SshMultiplexingHelper.php +++ b/app/Helpers/SshMultiplexingHelper.php @@ -28,15 +28,20 @@ class SshMultiplexingHelper public static function removeMuxFile(Server $server): void { - $closeCommand = 'ssh -O exit -o ControlPath='.self::muxSocket($server).' '; - if (data_get($server, 'settings.is_cloudflare_tunnel')) { - $closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" '; - } - $closeCommand .= self::escapedUserAtHost($server); - + $closeCommand = self::muxControlCommand($server, 'exit'); Process::run($closeCommand); } + private static function muxControlCommand(Server $server, string $operation): string + { + $command = "ssh -O {$operation} -o ControlPath=".self::muxSocket($server).' '; + if (data_get($server, 'settings.is_cloudflare_tunnel')) { + $command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" '; + } + + return $command.self::escapedUserAtHost($server); + } + public static function generateScpCommand(Server $server, string $source, string $dest): string { $sshConfig = self::serverSshConfiguration($server); @@ -128,24 +133,31 @@ class SshMultiplexingHelper $lockDirectory = self::muxLockDirectory($server); $lockTimeout = (int) config('constants.ssh.mux_lock_timeout'); + $checkCommand = self::muxControlCommand($server, 'check'); + $script = <<<'SH' cmd=$1 socket=$2 lock=$3 timeout=$4 +check=$5 run_command() { sh -c "$cmd" } -if [ -S "$socket" ]; then +mux_ready() { + [ -S "$socket" ] && sh -c "$check" >/dev/null 2>&1 +} + +if mux_ready; then run_command exit $? fi waited=0 while ! mkdir "$lock" 2>/dev/null; do - if [ -S "$socket" ]; then + if mux_ready; then run_command exit $? fi @@ -171,7 +183,7 @@ sh -c "$cmd" & child=$! for _ in 1 2 3 4 5 6 7 8 9 10; do - if [ -S "$socket" ] || ! kill -0 "$child" 2>/dev/null; then + if mux_ready || ! kill -0 "$child" 2>/dev/null; then break fi sleep 0.1 @@ -186,7 +198,8 @@ SH; .escapeshellarg($command).' ' .escapeshellarg($muxSocket).' ' .escapeshellarg($lockDirectory).' ' - .escapeshellarg((string) $lockTimeout); + .escapeshellarg((string) $lockTimeout).' ' + .escapeshellarg($checkCommand); } private static function escapedUserAtHost(Server $server): string diff --git a/tests/Feature/SshMultiplexingLockTest.php b/tests/Feature/SshMultiplexingLockTest.php index c39d5a48f..ff8ff20bc 100644 --- a/tests/Feature/SshMultiplexingLockTest.php +++ b/tests/Feature/SshMultiplexingLockTest.php @@ -71,8 +71,8 @@ it('adds native openssh multiplexing options to ssh commands', function () { ->toContain("-o ControlPath=/var/www/html/storage/app/ssh/mux/mux_{$server->uuid}") ->toContain("/var/www/html/storage/app/ssh/mux/mux_{$server->uuid}.lock") ->toContain('-o ControlPersist=3600') - ->not->toContain('ssh -fN') - ->not->toContain('-O check'); + ->toContain('-O check') + ->not->toContain('ssh -fN'); Process::assertNothingRan(); }); @@ -105,8 +105,8 @@ it('adds native openssh multiplexing options to scp commands', function () { ->toContain("-o ControlPath=/var/www/html/storage/app/ssh/mux/mux_{$server->uuid}") ->toContain("/var/www/html/storage/app/ssh/mux/mux_{$server->uuid}.lock") ->toContain('-o ControlPersist=3600') - ->not->toContain('ssh -fN') - ->not->toContain('-O check'); + ->toContain('-O check') + ->not->toContain('ssh -fN'); Process::assertNothingRan(); });