From 989c2238f8c4a11a06eb6d62a3a4685c6b5d70ba Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:19:42 +0100 Subject: [PATCH] fix(storages): connect to SFTP hosts on IPv6 and show the network error Co-Authored-By: Claude Opus 5.5 --- .changes/failed-connection-to-sftp-backup-storages.md | 6 ++++++ app/Models/S3Storage.php | 10 ++++++++++ app/Support/PinnedSftpConnectionProvider.php | 3 ++- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 .changes/failed-connection-to-sftp-backup-storages.md diff --git a/.changes/failed-connection-to-sftp-backup-storages.md b/.changes/failed-connection-to-sftp-backup-storages.md new file mode 100644 index 0000000000..3979bcbd72 --- /dev/null +++ b/.changes/failed-connection-to-sftp-backup-storages.md @@ -0,0 +1,6 @@ +--- +type: fixed +--- +Failed connection to SFTP backup storages on an IPv6 address, and a connection error without the network reason + +- The error now shows the network reason, for example `Connection refused` or `Connection timed out` diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php index 0a2c977fbf..45d408548c 100644 --- a/app/Models/S3Storage.php +++ b/app/Models/S3Storage.php @@ -26,6 +26,7 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use League\Flysystem\Filesystem; use League\Flysystem\FilesystemAdapter as FlysystemAdapter; +use phpseclib3\Exception\UnableToConnectException; class S3Storage extends BaseModel { @@ -956,6 +957,15 @@ class S3Storage extends BaseModel private function toUserFriendlyConnectionException(\Throwable $exception): \Throwable { + $rootCause = $exception; + while ($rootCause->getPrevious() !== null) { + $rootCause = $rootCause->getPrevious(); + } + if ($rootCause instanceof UnableToConnectException) { + // Flysystem shows only "Unable to connect to host". The network error (refused, timed out) is in phpseclib. + return new \RuntimeException('Could not connect to the '.$this->storageType()->label().' server: '.$rootCause->getMessage(), previous: $exception); + } + $message = str($exception->getMessage())->lower(); if ($message->contains(['timed out', 'timeout', 'connection refused', 'could not resolve', 'curl error 28'])) { diff --git a/app/Support/PinnedSftpConnectionProvider.php b/app/Support/PinnedSftpConnectionProvider.php index 08c2858a9e..6c7b821d78 100644 --- a/app/Support/PinnedSftpConnectionProvider.php +++ b/app/Support/PinnedSftpConnectionProvider.php @@ -29,7 +29,8 @@ class PinnedSftpConnectionProvider implements ConnectionProvider public static function create(string $host, string $address, int $port, string $username, ?string $password, ?string $privateKey, ?string $pinnedHostKey, int $timeout): self { $provider = app(SftpConnectionProvider::class, [ - 'host' => $address, + // phpseclib passes the host to fsockopen(), which needs brackets around an IPv6 address. + 'host' => filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false ? "[{$address}]" : $address, 'username' => $username, 'password' => $password, 'privateKey' => $privateKey,