Fix: SSH multiplexing

This commit is contained in:
peaklabs-dev
2024-09-17 12:26:11 +02:00
parent f9375f91ec
commit 144508218e
7 changed files with 92 additions and 89 deletions

View File

@@ -3,11 +3,10 @@
namespace App\Helpers;
use App\Models\Server;
use App\Models\PrivateKey;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use App\Models\PrivateKey;
class SshMultiplexingHelper
{
@@ -15,7 +14,8 @@ class SshMultiplexingHelper
public static function serverSshConfiguration(Server $server)
{
$sshKeyLocation = $server->privateKey->getKeyLocation();
$privateKey = PrivateKey::findOrFail($server->private_key_id);
$sshKeyLocation = $privateKey->getKeyLocation();
$muxFilename = '/var/www/html/storage/app/ssh/mux/' . $server->muxFilename();
return [
@@ -26,15 +26,21 @@ class SshMultiplexingHelper
public static function ensureMultiplexedConnection(Server $server)
{
if (!self::isMultiplexingEnabled()) {
ray('Multiplexing is disabled');
return;
}
ray('Ensuring multiplexed connection for server: ' . $server->id);
$sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename'];
$sshKeyLocation = $sshConfig['sshKeyLocation'];
if (!file_exists($sshKeyLocation)) {
throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
}
self::validateSshKey($sshKeyLocation);
if (isset(self::$ensuredConnections[$server->id]) && !self::shouldResetMultiplexedConnection($server)) {
ray('Existing connection is still valid');
return;
}
@@ -42,6 +48,7 @@ class SshMultiplexingHelper
$fileCheckProcess = Process::run($checkFileCommand);
if ($fileCheckProcess->exitCode() !== 0) {
ray('Mux socket file not found, establishing new connection');
self::establishNewMultiplexedConnection($server);
return;
}
@@ -49,19 +56,22 @@ class SshMultiplexingHelper
$checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip}";
$process = Process::run($checkCommand);
if ($process->exitCode() === 0) {
if ($process->exitCode() !== 0) {
ray('Existing connection check failed, establishing new connection');
self::establishNewMultiplexedConnection($server);
} else {
ray('Existing connection is valid');
self::$ensuredConnections[$server->id] = [
'timestamp' => now(),
'muxSocket' => $muxSocket,
];
return;
}
self::establishNewMultiplexedConnection($server);
}
public static function establishNewMultiplexedConnection(Server $server)
{
ray('Establishing new multiplexed connection for server: ' . $server->id);
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
@@ -84,9 +94,12 @@ class SshMultiplexingHelper
$establishProcess = Process::run($establishCommand);
if ($establishProcess->exitCode() !== 0) {
ray('Failed to establish multiplexed connection', $establishProcess->errorOutput());
throw new \RuntimeException('Failed to establish multiplexed connection: ' . $establishProcess->errorOutput());
}
ray('Multiplexed connection established successfully');
$muxContent = "Multiplexed connection established at " . now()->toDateTimeString();
Storage::disk('ssh-mux')->put(basename($muxSocket), $muxContent);
@@ -99,6 +112,7 @@ class SshMultiplexingHelper
public static function shouldResetMultiplexedConnection(Server $server)
{
if (!(config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) {
ray('Multiplexing is disabled or running on Windows Docker Desktop');
return false;
}
@@ -110,7 +124,9 @@ class SshMultiplexingHelper
$muxPersistTime = config('constants.ssh.mux_persist_time');
$resetInterval = strtotime($muxPersistTime) - time();
return $lastEnsured->addSeconds($resetInterval)->isPast();
$shouldReset = $lastEnsured->addSeconds($resetInterval)->isPast();
ray('Should reset multiplexed connection', ['server_id' => $server->id, 'should_reset' => $shouldReset]);
return $shouldReset;
}
public static function removeMuxFile(Server $server)
@@ -130,38 +146,22 @@ class SshMultiplexingHelper
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
$user = $server->user;
$port = $server->port;
$timeout = config('constants.ssh.command_timeout');
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command = "timeout $timeout scp ";
$muxEnabled = config('constants.ssh.mux_enabled', true) && config('coolify.is_windows_docker_desktop') == false;
ray('SSH Multiplexing Enabled:', $muxEnabled)->blue();
if ($muxEnabled) {
if (self::isMultiplexingEnabled()) {
$muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
ray('Using SSH Multiplexing')->green();
} else {
ray('Not using SSH Multiplexing')->red();
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$scp_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
$scp_command .= "-i {$sshKeyLocation} "
.'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
.'-o PasswordAuthentication=no '
."-o ConnectTimeout=$connectionTimeout "
."-o ServerAliveInterval=$serverInterval "
.'-o RequestTTY=no '
.'-o LogLevel=ERROR '
."-P {$port} "
."{$source} "
."{$user}@{$server->ip}:{$dest}";
self::addCloudflareProxyCommand($scp_command, $server);
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
$scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
return $scp_command;
}
@@ -179,45 +179,61 @@ class SshMultiplexingHelper
$timeout = config('constants.ssh.command_timeout');
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$muxEnabled = config('constants.ssh.mux_enabled') && !config('coolify.is_windows_docker_desktop');
ray('Config MUX Enabled:', config('constants.ssh.mux_enabled'));
ray('Config Windows Docker Desktop:', config('coolify.is_windows_docker_desktop'));
ray('MUX Enabled:', $muxEnabled);
$ssh_command = "timeout $timeout ssh ";
ray('SSH Multiplexing Enabled:', $muxEnabled)->blue();
if ($muxEnabled) {
if (self::isMultiplexingEnabled()) {
$muxPersistTime = config('constants.ssh.mux_persist_time');
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
ray('Using SSH Multiplexing')->green();
} else {
ray('Not using SSH Multiplexing')->red();
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
self::addCloudflareProxyCommand($ssh_command, $server);
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$delimiter = Hash::make($command);
$command = str_replace($delimiter, '', $command);
$ssh_command .= "-i {$sshKeyLocation} "
$ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL
.$command.PHP_EOL
.$delimiter;
return $ssh_command;
}
private static function isMultiplexingEnabled(): bool
{
return config('constants.ssh.mux_enabled') && !config('coolify.is_windows_docker_desktop');
}
private static function validateSshKey(string $sshKeyLocation): void
{
$checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
$keyCheckProcess = Process::run($checkKeyCommand);
if ($keyCheckProcess->exitCode() !== 0) {
throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
}
}
private static function addCloudflareProxyCommand(string &$command, Server $server): void
{
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
}
}
private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval): string
{
return "-i {$sshKeyLocation} "
.'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
.'-o PasswordAuthentication=no '
."-o ConnectTimeout=$connectionTimeout "
."-o ServerAliveInterval=$serverInterval "
.'-o RequestTTY=no '
.'-o LogLevel=ERROR '
."-p {$server->port} "
."{$server->user}@{$server->ip} "
." 'bash -se' << \\$delimiter".PHP_EOL
.$command.PHP_EOL
.$delimiter;
return $ssh_command;
."-p {$server->port} ";
}
}