artemyurov / laravel-autossh-tunnel
Modern SSH Tunnel Manager for Laravel with autossh support and automatic lifecycle management
Installs: 2
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/artemyurov/laravel-autossh-tunnel
Requires
- php: ^8.2
- illuminate/config: ^10.0|^11.0|^12.0
- illuminate/log: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- symfony/process: ^6.0|^7.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
This package is auto-updated.
Last update: 2025-12-16 18:05:46 UTC
README
Modern SSH Tunnel Manager for Laravel with autossh support and automatic lifecycle management.
Features
- π Automatic tunnel lifecycle management via callback pattern
- π AutoSSH support for automatic reconnection
- π‘οΈ Comprehensive error handling with detailed messages
- π Port availability checking
- ποΈ Laravel Database connections integration
- βοΈ Flexible configuration (env, config files or direct parameters)
- β Configuration validation
- π Detailed logging
- π― Multiple simultaneous tunnels support
Installation
composer require artemyurov/laravel-autossh-tunnel
Publish Configuration
# Publish config and .env example php artisan vendor:publish --tag=tunnel # Or publish separately php artisan vendor:publish --tag=tunnel-config # config/tunnel.php only php artisan vendor:publish --tag=tunnel-env # .env.example.tunnel only
After publishing, copy the tunnel environment variables to your .env:
cat .env.example.tunnel >> .env # Then edit .env with your actual credentials
Configuration
Environment Variables
The configuration uses a clear logical order:
- SSH Connection - How to connect to the SSH server
- Remote/Local - What to forward and where
- SSH Options - Connection behavior settings
# Default tunnel connection TUNNEL_CONNECTION=remote_db TUNNEL_DEBUG=false TUNNEL_AUTOSSH_ENABLED=true # SSH Connection (how to connect) TUNNEL_SSH_USER=your_ssh_user TUNNEL_SSH_HOST=your_server.com TUNNEL_SSH_PORT=22 TUNNEL_SSH_KEY=/path/to/ssh/key # Remote Target (what to forward on SSH server) TUNNEL_REMOTE_HOST=localhost TUNNEL_REMOTE_PORT=5432 # Local Bind (where to bind locally) TUNNEL_LOCAL_HOST=127.0.0.1 TUNNEL_LOCAL_PORT=15432 # SSH Options TUNNEL_SSH_STRICT_HOST_KEY_CHECKING=false TUNNEL_SSH_SERVER_ALIVE_INTERVAL=60 TUNNEL_SSH_SERVER_ALIVE_COUNT_MAX=3 TUNNEL_SSH_EXIT_ON_FORWARD_FAILURE=true TUNNEL_SSH_TCP_KEEP_ALIVE=true TUNNEL_SSH_CONNECT_TIMEOUT=10 # Database connection using tunnel TUNNEL_DB_CONNECTION=pgsql TUNNEL_DB_HOST="${TUNNEL_LOCAL_HOST}" TUNNEL_DB_PORT="${TUNNEL_LOCAL_PORT}" TUNNEL_DB_DATABASE=database_name TUNNEL_DB_USERNAME=db_user TUNNEL_DB_PASSWORD=db_password
Configuration File
After publishing the config, edit config/tunnel.php:
return [ // Default tunnel name 'default' => env('TUNNEL_CONNECTION', 'remote_db'), // Enable detailed logging 'debug' => env('TUNNEL_DEBUG', env('APP_DEBUG', false)), // AutoSSH configuration 'autossh' => [ 'enabled' => env('TUNNEL_AUTOSSH_ENABLED', true), ], // Retry configuration for database operations 'retry' => [ 'max_attempts' => env('TUNNEL_RETRY_MAX_ATTEMPTS', 3), 'delay' => env('TUNNEL_RETRY_DELAY', 2), 'exponential' => env('TUNNEL_RETRY_EXPONENTIAL', false), ], // Connection validation settings 'validation' => [ 'port_timeout' => env('TUNNEL_VALIDATION_PORT_TIMEOUT', 1), 'database_timeout' => env('TUNNEL_VALIDATION_DATABASE_TIMEOUT', 5), 'database_max_attempts' => env('TUNNEL_VALIDATION_DATABASE_MAX_ATTEMPTS', 5), 'database_retry_delay' => env('TUNNEL_VALIDATION_DATABASE_RETRY_DELAY', 2), ], // Signal handling (SIGINT, SIGTERM) 'signals' => [ 'enabled' => env('TUNNEL_SIGNALS_ENABLED', true), 'handlers' => ['SIGINT', 'SIGTERM'], ], // Tunnel reuse settings 'reuse' => [ 'use_pid_file' => env('TUNNEL_REUSE_PID_FILE', true), 'use_port_scan' => env('TUNNEL_REUSE_PORT_SCAN', true), 'pid_directory' => env('TUNNEL_PID_DIRECTORY', sys_get_temp_dir() . '/laravel-autossh-tunnel'), ], 'connections' => [ 'remote_db' => [ 'type' => 'forward', // SSH Connection 'user' => env('TUNNEL_SSH_USER'), 'host' => env('TUNNEL_SSH_HOST'), 'port' => env('TUNNEL_SSH_PORT', 22), 'identity_file' => env('TUNNEL_SSH_KEY'), // Remote Target 'remote_host' => env('TUNNEL_REMOTE_HOST', 'localhost'), 'remote_port' => env('TUNNEL_REMOTE_PORT', 5432), // Local Bind 'local_host' => env('TUNNEL_LOCAL_HOST', '127.0.0.1'), 'local_port' => env('TUNNEL_LOCAL_PORT', 15432), // SSH Options (all optional, defaults shown) 'ssh_options' => [ 'StrictHostKeyChecking' => env('TUNNEL_SSH_STRICT_HOST_KEY_CHECKING', false), 'ServerAliveInterval' => env('TUNNEL_SSH_SERVER_ALIVE_INTERVAL', 60), 'ServerAliveCountMax' => env('TUNNEL_SSH_SERVER_ALIVE_COUNT_MAX', 3), 'ExitOnForwardFailure' => env('TUNNEL_SSH_EXIT_ON_FORWARD_FAILURE', true), 'TCPKeepAlive' => env('TUNNEL_SSH_TCP_KEEP_ALIVE', true), 'ConnectTimeout' => env('TUNNEL_SSH_CONNECT_TIMEOUT', 10), ], ], 'local_webhooks' => [ 'type' => 'reverse', 'user' => env('WEBHOOK_SSH_USER'), 'host' => env('WEBHOOK_SSH_HOST'), 'port' => env('WEBHOOK_SSH_PORT', 22), 'identity_file' => env('WEBHOOK_SSH_KEY'), 'remote_host' => env('WEBHOOK_REMOTE_HOST', 'localhost'), // Or 0.0.0.0 for public access 'remote_port' => env('WEBHOOK_REMOTE_PORT', 8080), 'local_host' => env('WEBHOOK_LOCAL_HOST', '127.0.0.1'), 'local_port' => env('WEBHOOK_LOCAL_PORT', 8000), 'ssh_options' => [ 'StrictHostKeyChecking' => env('WEBHOOK_SSH_STRICT_HOST_KEY_CHECKING', false), 'ServerAliveInterval' => env('WEBHOOK_SSH_SERVER_ALIVE_INTERVAL', 60), 'ServerAliveCountMax' => env('WEBHOOK_SSH_SERVER_ALIVE_COUNT_MAX', 3), 'ExitOnForwardFailure' => env('WEBHOOK_SSH_EXIT_ON_FORWARD_FAILURE', true), 'TCPKeepAlive' => env('WEBHOOK_SSH_TCP_KEEP_ALIVE', true), 'ConnectTimeout' => env('WEBHOOK_SSH_CONNECT_TIMEOUT', 10), ], ], ], ];
Multiple Connections Example
For multiple tunnel connections, use unique prefixes for each connection:
# PostgreSQL Development Server REMOTE_DEV_SSH_USER=www-backend REMOTE_DEV_SSH_HOST=dev.example.com REMOTE_DEV_SSH_PORT=22 REMOTE_DEV_SSH_KEY= REMOTE_DEV_REMOTE_HOST=localhost REMOTE_DEV_REMOTE_PORT=5432 REMOTE_DEV_LOCAL_HOST=127.0.0.1 REMOTE_DEV_LOCAL_PORT=16432 REMOTE_DEV_DB_DATABASE=project_db REMOTE_DEV_DB_USERNAME=db_user REMOTE_DEV_DB_PASSWORD=secret # MySQL Legacy Database LEGACY_SSH_USER=root LEGACY_SSH_HOST=legacy.example.com LEGACY_SSH_PORT=22 LEGACY_REMOTE_HOST=127.0.0.1 LEGACY_REMOTE_PORT=3306 LEGACY_LOCAL_HOST=127.0.0.1 LEGACY_LOCAL_PORT=13306 LEGACY_DB_DATABASE=legacy_db LEGACY_DB_USERNAME=legacy_user LEGACY_DB_PASSWORD=secret
// config/tunnel.php 'connections' => [ 'remote_dev_db' => [ 'type' => 'forward', 'user' => env('REMOTE_DEV_SSH_USER'), 'host' => env('REMOTE_DEV_SSH_HOST'), 'port' => env('REMOTE_DEV_SSH_PORT', 22), 'identity_file' => env('REMOTE_DEV_SSH_KEY'), 'remote_host' => env('REMOTE_DEV_REMOTE_HOST', 'localhost'), 'remote_port' => env('REMOTE_DEV_REMOTE_PORT', 5432), 'local_host' => env('REMOTE_DEV_LOCAL_HOST', '127.0.0.1'), 'local_port' => env('REMOTE_DEV_LOCAL_PORT', 16432), ], 'legacy_db' => [ 'type' => 'forward', 'user' => env('LEGACY_SSH_USER'), 'host' => env('LEGACY_SSH_HOST'), 'port' => env('LEGACY_SSH_PORT', 22), 'identity_file' => env('LEGACY_SSH_KEY'), 'remote_host' => env('LEGACY_REMOTE_HOST', '127.0.0.1'), 'remote_port' => env('LEGACY_REMOTE_PORT', 3306), 'local_host' => env('LEGACY_LOCAL_HOST', '127.0.0.1'), 'local_port' => env('LEGACY_LOCAL_PORT', 13306), ], ],
Tunnel Types
The package supports two types of SSH tunnels:
Forward Tunnel (-L) - Access Remote Services
Forward tunnels allow you to access remote services from your local machine.
βββββββββββββββ SSH Tunnel βββββββββββββββ
β Local β ββββββββββββββββββββββββββ> β Remote β
β Machine β localhost:15432 β Server β
β β β β
β β β PostgreSQL β
β β <βββββββββββββββββββββββββ β :5432 β
βββββββββββββββ βββββββββββββββ
SSH Command:
ssh -L 15432:localhost:5432 user@server.com
β β β
β β ββ remote_port (ΠΏΠΎΡΡ Π½Π° ΡΠ΄Π°Π»ΡΠ½Π½ΠΎΠΌ ΡΠ΅ΡΠ²Π΅ΡΠ΅)
β βββββββββββ remote_host (Ρ
ΠΎΡΡ Π½Π° ΡΠ΄Π°Π»ΡΠ½Π½ΠΎΠΌ ΡΠ΅ΡΠ²Π΅ΡΠ΅)
ββββββββββββββββββ local_port (ΠΏΠΎΡΡ Π½Π° Π»ΠΎΠΊΠ°Π»ΡΠ½ΠΎΠΉ ΠΌΠ°ΡΠΈΠ½Π΅)
Use Cases:
- Access production/staging databases for debugging
- Connect to internal APIs not exposed to the internet
- Access remote services (Redis, Elasticsearch, etc.)
- Secure connection to remote development environments
Example:
use ArtemYurov\Autossh\Facades\Tunnel; // Access remote database Tunnel::connection('remote_db')->execute(function() { // Connect to remote PostgreSQL via localhost:15432 $users = DB::connection('pgsql_remote')->table('users')->get(); });
Reverse Tunnel (-R) - Expose Local Services
Reverse tunnels expose your local application to a remote server, making it accessible from the internet.
βββββββββββββββ SSH Tunnel βββββββββββββββ
β Local β <ββββββββββββββββββββββββββ β Remote β
β Machine β β Server β
β β β β
β Laravel β β Public IP β
β :8000 β βββββββββββββββββββββββ> β :8080 β
βββββββββββββββ βββββββββββββββ
β
Webhooks from:
β’ Stripe
β’ GitHub
β’ Telegram
β’ PayPal
SSH Command:
ssh -R 8080:localhost:8000 user@server.com
β β β
β β ββ local_port (ΠΏΠΎΡΡ Π»ΠΎΠΊΠ°Π»ΡΠ½ΠΎΠΉ ΠΌΠ°ΡΠΈΠ½Ρ)
β βββββββββββ remote_host (bind Π°Π΄ΡΠ΅Ρ Π½Π° ΡΠ΄Π°Π»ΡΠ½Π½ΠΎΠΌ ΡΠ΅ΡΠ²Π΅ΡΠ΅)
ββββββββββββββββββ remote_port (ΠΏΠΎΡΡ Π½Π° ΡΠ΄Π°Π»ΡΠ½Π½ΠΎΠΌ ΡΠ΅ΡΠ²Π΅ΡΠ΅)
Important: remote_host is the bind address on the remote server:
localhost- tunnel accessible only locally on remote server (127.0.0.1:8080)0.0.0.0- tunnel publicly accessible (your-server.com:8080 from internet)
Use Cases:
- Test webhooks locally (Stripe, PayPal, GitHub, Telegram)
- Demo local application to clients without deployment
- Temporary public access to development environment
- Receive callbacks from external services
Example:
use ArtemYurov\Autossh\Facades\Tunnel; // Expose local Laravel app for webhook testing Tunnel::connection('local_webhooks')->execute(function() { $this->info('Local app is now accessible at http://your-server.com:8080'); $this->info('Configure webhooks to point to this URL'); // Keep tunnel open while testing sleep(3600); // 1 hour });
Usage
Callback Pattern (Recommended)
Automatic tunnel closure after execution:
use ArtemYurov\Autossh\Facades\Tunnel; // Using configuration from config/tunnel.php Tunnel::connection('remote_db')->execute(function() { // Tunnel is active, you can work with remote service // Tunnel will automatically close after execution });
With Laravel Database Integration
use ArtemYurov\Autossh\Facades\Tunnel; use Illuminate\Support\Facades\DB; Tunnel::connection('remote_db') ->withDatabaseConnection('pgsql_remote', [ 'driver' => 'pgsql', 'database' => env('REMOTE_DB_DATABASE'), 'username' => env('REMOTE_DB_USERNAME'), 'password' => env('REMOTE_DB_PASSWORD'), ]) ->execute(function() { // Now you can use the connection $users = DB::connection('pgsql_remote')->table('users')->get(); // Tunnel will automatically close after execution });
Manual Management
use ArtemYurov\Autossh\Facades\Tunnel; $connection = Tunnel::connection('remote_db')->start(); try { // Work with tunnel $pid = $connection->getPid(); $isRunning = $connection->isRunning(); } finally { // Must close the tunnel $connection->stop(); }
Multiple Tunnels Simultaneously
use ArtemYurov\Autossh\Tunnel; // PostgreSQL tunnel $pgTunnel = Tunnel::connection('pgsql')->start(); // MySQL tunnel $mysqlTunnel = Tunnel::connection('mysql')->start(); try { // Work with both tunnels } finally { $pgTunnel->stop(); $mysqlTunnel->stop(); }
Using in Artisan Commands
namespace App\Console\Commands; use Illuminate\Console\Command; use ArtemYurov\Autossh\Facades\Tunnel; use Illuminate\Support\Facades\DB; class SyncDatabase extends Command { protected $signature = 'db:sync'; public function handle(): int { return Tunnel::connection('remote_db') ->withDatabaseConnection('remote_db', [ 'driver' => 'pgsql', 'database' => env('REMOTE_DB_DATABASE'), 'username' => env('REMOTE_DB_USERNAME'), 'password' => env('REMOTE_DB_PASSWORD'), ]) ->execute(function() { $this->info('Syncing database...'); // Your sync logic $tables = DB::connection('remote_db') ->select("SELECT tablename FROM pg_tables WHERE schemaname = 'public'"); $this->info('Found tables: ' . count($tables)); return Command::SUCCESS; }); } }
Long-Running Tunnels
For persistent tunnels that stay active (similar to ngrok), use the Artisan commands:
Start with Live Monitoring
Start a tunnel with real-time status updates (like ngrok):
php artisan tunnel:start
# or specify connection
php artisan tunnel:start remote_db
This will show a live dashboard with tunnel information:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SSH Tunnel Monitor β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£
β Connection: remote_db β
β Local Port: 15432 β
β Remote: localhost:5432 β
β SSH: user@example.com β
β PID: 12345 β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£
β Press Ctrl+C to stop the tunnel β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Status: β ACTIVE | Uptime: 2m 34s | PID: 12345
Press Ctrl+C to gracefully stop the tunnel.
Start in Background (Daemon Mode)
Run tunnel in background without monitoring (detached daemon mode):
php artisan tunnel:start --detach
# or
php artisan tunnel:start remote_db --detach
Check Tunnel Status
View status of running tunnels:
# Check specific tunnel php artisan tunnel:status php artisan tunnel:status remote_db # Check all running tunnels php artisan tunnel:status --all
Stop Tunnel
Stop a running tunnel:
php artisan tunnel:stop
php artisan tunnel:stop remote_db
# Stop all tunnels
php artisan tunnel:stop --all
Run as System Service
For production environments, you can run the tunnel as a persistent systemd service.
π See detailed guide: SYSTEMD.md
Quick example:
# Create service file sudo nano /etc/systemd/system/ssh-tunnel.service # Enable and start sudo systemctl enable ssh-tunnel sudo systemctl start ssh-tunnel
Related Resources
- Running as systemd Service - Complete guide for production deployments
- Self-hosted ngrok alternative - Building your own tunnel infrastructure
AutoSSH
The package automatically detects autossh availability and uses it instead of regular ssh to provide automatic reconnection on connection loss.
Installing autossh
macOS:
brew install autossh
Ubuntu/Debian:
apt-get install autossh
The package uses command -v autossh to detect autossh automatically. No additional configuration needed.
Disable AutoSSH
If you want to use regular SSH even when autossh is available:
TUNNEL_AUTOSSH_ENABLED=false
Error Handling
use ArtemYurov\Autossh\Facades\Tunnel; use ArtemYurov\Autossh\Exceptions\TunnelConnectionException; use ArtemYurov\Autossh\Exceptions\TunnelConfigException; try { Tunnel::connection('remote_db')->execute(function() { // Your code }); } catch (TunnelConfigException $e) { // Configuration error (invalid port, missing key, etc.) Log::error('Tunnel configuration error: ' . $e->getMessage()); } catch (TunnelConnectionException $e) { // Connection error (port occupied, SSH failed, etc.) Log::error('Tunnel connection error: ' . $e->getMessage()); }
Important Notes
MySQL/MariaDB: localhost vs 127.0.0.1
Critical: In MySQL and MariaDB, localhost has a special hardcoded meaning - it always represents a Unix socket connection, not a TCP/IP connection. This behavior cannot be changed.
The Difference
localhostβ Unix socket connection (/var/run/mysqld/mysqld.sock)127.0.0.1β TCP/IP connection over loopback interface
When using SSH tunnels with MySQL/MariaDB:
// β WRONG - Will attempt Unix socket, not tunnel Tunnel::connection('mysql_tunnel') ->withDatabaseConnection('mysql_remote', [ 'driver' => 'mysql', 'host' => 'localhost', // β Unix socket 'port' => 13306, ]); // β CORRECT - Will use TCP/IP through tunnel Tunnel::connection('mysql_tunnel') ->withDatabaseConnection('mysql_remote', [ 'driver' => 'mysql', 'host' => '127.0.0.1', // β TCP/IP 'port' => 13306, ]);
Configuration Example
Environment Variables:
# Remote database credentials MYSQL_REMOTE_HOST=127.0.0.1 # β Use 127.0.0.1, not localhost MYSQL_REMOTE_PORT=3306 MYSQL_REMOTE_DATABASE=mydb MYSQL_REMOTE_USERNAME=myuser MYSQL_REMOTE_PASSWORD=secret # SSH Tunnel settings MYSQL_SSH_USER=sshuser MYSQL_SSH_HOST=remote-server.com MYSQL_SSH_PORT=22 # Local tunnel bind MYSQL_LOCAL_HOST=127.0.0.1 # β Bind to specific IPv4 address MYSQL_LOCAL_PORT=13306
Tunnel Configuration:
// config/tunnel.php 'mysql_remote' => [ 'type' => 'forward', 'user' => env('MYSQL_SSH_USER'), 'host' => env('MYSQL_SSH_HOST'), 'port' => env('MYSQL_SSH_PORT', 22), // β IMPORTANT: Use 127.0.0.1 for MySQL/MariaDB 'remote_host' => env('MYSQL_REMOTE_HOST', '127.0.0.1'), 'remote_port' => env('MYSQL_REMOTE_PORT', 3306), 'local_host' => env('MYSQL_LOCAL_HOST', '127.0.0.1'), 'local_port' => env('MYSQL_LOCAL_PORT', 13306), ],
Database User Permissions
MySQL/MariaDB treat user@localhost and user@127.0.0.1 as different users:
-- Unix socket access (localhost means socket) CREATE USER 'myuser'@'localhost' IDENTIFIED BY 'password'; -- TCP/IP access (required for SSH tunnels) CREATE USER 'myuser'@'127.0.0.1' IDENTIFIED BY 'password'; -- Grant permissions GRANT ALL PRIVILEGES ON mydb.* TO 'myuser'@'127.0.0.1'; FLUSH PRIVILEGES;
Important: When using SSH tunnels to access remote MySQL/MariaDB:
- Create database user with
@127.0.0.1host (not@localhost) - Set
remote_hostto127.0.0.1in tunnel config - Use
'host' => '127.0.0.1'in database connection config
Why This Matters
This issue commonly occurs when:
- Remote server's
localhostresolves to IPv6 (::1) - Database user only has
@localhost(Unix socket) permissions - Database rejects connections: "Host '::1' is not allowed to connect"
Solution: Always use 127.0.0.1 for both remote_host in tunnel config and database connection host when working with MySQL/MariaDB over SSH tunnels.
Advanced Features
Tunnel Reuse
The package can automatically detect and reuse existing SSH tunnels instead of creating new ones. This is useful for:
- Avoiding "port already in use" errors
- Sharing tunnels between multiple processes
- Faster command execution (no tunnel startup time)
Smart Tunnel Discovery
The package uses two methods to find existing tunnels:
- PID file - Fastest method, stores tunnel PID in temp directory
- Port scan - Uses
lsof/netstatto find processes by port
use ArtemYurov\Autossh\Tunnel; // Automatically reuse existing tunnel or create new one $tunnel = Tunnel::connection('remote_db')->reuseOrCreate(); // Or explicitly find existing tunnel by port $pid = $tunnel->findExistingByPort(); if ($pid) { echo "Found existing tunnel with PID: $pid"; }
Reuse Command
Find and reuse existing tunnel from command line:
# Find and reuse existing tunnel php artisan tunnel:reuse remote_db # Reuse tunnel and register database connection php artisan tunnel:reuse remote_db \ --db-connection=pgsql_remote \ --db-database=mydb \ --db-username=user \ --db-password=pass
Retry Logic
Execute database operations with automatic retry on connection errors:
use ArtemYurov\Autossh\Tunnel; $tunnel = Tunnel::connection('remote_db') ->withDatabaseConnection('pgsql_remote', [...]) ->start(); // Execute with automatic retry on connection loss $result = $tunnel->executeWithRetry(function() { return DB::connection('pgsql_remote')->table('users')->count(); }, $maxAttempts = 3);
If the operation fails due to connection error, the tunnel will automatically reconnect and retry the operation.
Configuration
Configure retry behavior in config/tunnel.php:
'retry' => [ 'max_attempts' => 3, // Maximum retry attempts 'delay' => 2, // Delay between retries (seconds) 'exponential' => false, // Use exponential backoff (2s, 4s, 8s...) ],
Or use environment variables:
TUNNEL_RETRY_MAX_ATTEMPTS=3 TUNNEL_RETRY_DELAY=2 TUNNEL_RETRY_EXPONENTIAL=false
Database Validation
Verify that database is actually accessible through tunnel (not just port checking):
use ArtemYurov\Autossh\Tunnel; $connection = Tunnel::connection('remote_db') ->withDatabaseConnection('pgsql_remote', [...]) ->start(); // Simple validation (SELECT 1 query) if ($connection->validateDatabase('pgsql_remote')) { echo "Database is accessible"; } // Wait for database with retries if ($connection->waitForDatabase('pgsql_remote', $maxAttempts = 5, $delaySeconds = 2)) { echo "Database became available"; } // Full tunnel validation (process + port + database) $result = $connection->validate('pgsql_remote'); if ($result['valid']) { echo "Tunnel is fully operational"; } else { foreach ($result['errors'] as $error) { echo "Error: $error\n"; } }
Signal Handling
Tunnels can automatically handle system signals for graceful shutdown:
use ArtemYurov\Autossh\Tunnel; $connection = Tunnel::connection('remote_db') ->start() ->setupSignalHandlers(); // Handle SIGINT, SIGTERM // Tunnel will be properly closed when receiving Ctrl+C or kill signal
Requires pcntl extension. Configure in config/tunnel.php:
'signals' => [ 'enabled' => true, 'handlers' => ['SIGINT', 'SIGTERM'], ],
Keep-Alive Tunnels
Create tunnels that persist after script ends:
use ArtemYurov\Autossh\Tunnel; $connection = Tunnel::connection('remote_db') ->start() ->withKeepAlive(true); // Tunnel will stay alive even after script finishes echo "Tunnel PID: " . $connection->getPid();
To stop keep-alive tunnel, use the stop command:
php artisan tunnel:stop remote_db
Diagnostic Tools
Diagnose Command
Comprehensive tunnel health check:
# Basic diagnostics php artisan tunnel:diagnose remote_db # Include database check php artisan tunnel:diagnose remote_db --db-connection=pgsql_remote # Verbose output php artisan tunnel:diagnose remote_db --verbose
The diagnostic tool checks:
- β Configuration existence
- β Process running (PID file + port scan)
- β Process is SSH
- β Port accessibility
- β Database accessibility (optional)
ManagesTunnel Trait for Commands
Convenient trait for managing tunnels in Laravel commands:
namespace App\Console\Commands; use Illuminate\Console\Command; use ArtemYurov\Autossh\Console\Traits\ManagesTunnel; use Illuminate\Support\Facades\DB; class SyncRemoteData extends Command { use ManagesTunnel; protected $signature = 'data:sync'; public function handle(): int { // Setup tunnel with automatic reconnection and validation $this->setupTunnel('remote_db', [ 'connection_name' => 'pgsql_remote', 'driver' => 'pgsql', 'database' => env('REMOTE_DB_DATABASE'), 'username' => env('REMOTE_DB_USERNAME'), 'password' => env('REMOTE_DB_PASSWORD'), ], $keepAlive = false, $validateDb = true); try { // Execute with automatic retry on connection errors $this->withTunnelRetry(function() { $data = DB::connection('pgsql_remote') ->table('users') ->get(); $this->info("Synced " . count($data) . " records"); }); return Command::SUCCESS; } finally { // Graceful tunnel closure $this->closeTunnel(); } } }
ManagesTunnel Methods
setupTunnel($connection, $dbConfig, $keepAlive, $validateDb)- Initialize tunnelensureTunnelConnected($maxAttempts)- Check and reconnect if neededwithTunnelRetry($operation, $maxAttempts)- Execute with retryvalidateTunnelDatabase($connection, $timeout, $wait)- Validate databasevalidateTunnel($connection)- Full validationcloseTunnel()- Graceful shutdownisTunnelRunning()- Check statusgetTunnel()/getTunnelConnection()- Get instances
API Reference
Artisan Commands
tunnel:start {connection?} {--detach}- Start tunnel with live monitoring (or in background with --detach)tunnel:stop {connection?} {--all}- Stop tunnel (or all tunnels with --all)tunnel:status {connection?} {--all}- Show tunnel status (or all tunnels with --all)tunnel:reuse {connection?} {--db-connection=} {--db-driver=} {--db-database=} {--db-username=} {--db-password=}- Find and reuse existing tunneltunnel:diagnose {connection?} {--db-connection=} {--verbose}- Diagnose tunnel health
Tunnel
Static Methods
Tunnel::connection(?string $name = null): Tunnel- Create from config/tunnel.phpTunnel::fromConfig(TunnelConfig $config): Tunnel- Create from config object
Instance Methods
withDatabaseConnection(string $name, array $config): self- Register Laravel DB connectionstart(): TunnelConnection- Start tunnelreuseOrCreate(): TunnelConnection- Smart tunnel reuse or creationfindExistingByPort(): ?int- Find existing tunnel by port using lsofensureConnected(int $maxAttempts = 3): bool- Ensure tunnel is active, reconnect if neededexecute(callable $callback): mixed- Execute callback with automatic tunnel managementgetConfig(): TunnelConfig- Get configurationgetConnection(): ?TunnelConnection- Get active connection
TunnelConnection
isRunning(): bool- Check if tunnel is runninggetPid(): ?int- Get process PIDstop(): void- Stop tunnelverifyConnection(): bool- Verify tunnel availabilityensureConnected(int $maxAttempts = 3): bool- Reconnect if tunnel is downwithKeepAlive(bool $keepAlive = true): self- Set keep-alive flagsetupSignalHandlers(): self- Setup SIGINT/SIGTERM handlersvalidateDatabase(string $connectionName, int $timeout = 5): bool- Validate database accessibilitywaitForDatabase(string $connectionName, int $maxAttempts = 5, int $delaySeconds = 2): bool- Wait for database with retriesexecuteWithRetry(callable $operation, ?int $maxAttempts = null): mixed- Execute with automatic retryvalidate(?string $connectionName = null): array- Full validation (process + port + database)
TunnelManager
saveTunnel(string $name, TunnelConnection $connection): void- Save tunnel info to storagegetTunnelInfo(string $name): ?array- Get tunnel informationstopTunnel(string $name): bool- Stop tunnel by namegetAllTunnels(): array- Get all running tunnelsgetUptime(array $info): int- Get tunnel uptime in secondsformatUptime(int $seconds): string- Format uptime as human-readable string
Logging
The package uses standard Laravel Log facade. For detailed logging:
TUNNEL_DEBUG=true
Or in config/tunnel.php:
'debug' => true,
Requirements
- PHP ^8.2
- Laravel ^10.0|^11.0|^12.0
- SSH client installed on the system
- AutoSSH (optional, for auto-reconnection)
License
MIT License
Author
Artem Yurov (artem@yurov.org)