rds_ssm_connect 1.7.4 → 1.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,10 +28,6 @@ jobs:
28
28
  rust_target: x86_64-unknown-linux-gnu
29
29
  sidecar_target: node22-linux-x64
30
30
  sidecar_triple: x86_64-unknown-linux-gnu
31
- - platform: ubuntu-24.04-arm64
32
- rust_target: aarch64-unknown-linux-gnu
33
- sidecar_target: node22-linux-arm64
34
- sidecar_triple: aarch64-unknown-linux-gnu
35
31
  - platform: windows-latest
36
32
  rust_target: x86_64-pc-windows-msvc
37
33
  sidecar_target: node22-win-x64
package/README.md CHANGED
@@ -22,17 +22,85 @@ Secure database tunneling to AWS RDS through SSM port forwarding via bastion hos
22
22
 
23
23
  ## Installation
24
24
 
25
- ### Desktop App
25
+ ### macOS (Homebrew)
26
26
 
27
- Download the latest installer for your platform from [GitHub Releases](https://github.com/yarka-guru/connection_app/releases):
27
+ ```bash
28
+ brew tap yarka-guru/tap
29
+ brew install --cask rds-ssm-connect
30
+ ```
28
31
 
29
- | Platform | Format |
30
- |----------|--------|
31
- | macOS (Apple Silicon + Intel) | `.dmg` |
32
- | Windows | `.msi` / `.exe` |
33
- | Linux | `.deb` / `.AppImage` |
32
+ This installs the desktop app along with `aws-vault` and `awscli` dependencies. You also need the [Session Manager Plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html):
34
33
 
35
- ### CLI
34
+ ```bash
35
+ brew install --cask session-manager-plugin
36
+ ```
37
+
38
+ Or download the `.dmg` directly from [GitHub Releases](https://github.com/yarka-guru/connection_app/releases).
39
+
40
+ ### Linux
41
+
42
+ #### Option A: Homebrew
43
+
44
+ ```bash
45
+ # Install Homebrew if not already installed
46
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
47
+ echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> ~/.bashrc
48
+ eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
49
+
50
+ # Install the app (includes aws-vault and awscli as dependencies)
51
+ brew tap yarka-guru/tap
52
+ brew install yarka-guru/tap/rds-ssm-connect
53
+
54
+ # Install Session Manager Plugin (ARM64)
55
+ curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_arm64/session-manager-plugin.deb" -o session-manager-plugin.deb
56
+ sudo dpkg -i session-manager-plugin.deb
57
+ # For x86_64, replace ubuntu_arm64 with ubuntu_64bit
58
+
59
+ # Make brew tools visible to the desktop app
60
+ echo 'export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"' | sudo tee /etc/profile.d/linuxbrew.sh
61
+ # Log out and back in for this to take effect
62
+ ```
63
+
64
+ #### Option B: Direct .deb install
65
+
66
+ ```bash
67
+ # 1. Download and install the app
68
+ # ARM64:
69
+ wget https://github.com/yarka-guru/connection_app/releases/latest/download/RDS.SSM.Connect_1.7.5_arm64.deb
70
+ sudo dpkg -i RDS.SSM.Connect_1.7.5_arm64.deb
71
+ # x86_64:
72
+ # wget https://github.com/yarka-guru/connection_app/releases/latest/download/RDS.SSM.Connect_1.7.5_amd64.deb
73
+ # sudo dpkg -i RDS.SSM.Connect_1.7.5_amd64.deb
74
+
75
+ # 2. Install aws-vault
76
+ # ARM64:
77
+ wget https://github.com/99designs/aws-vault/releases/latest/download/aws-vault-linux-arm64 -O aws-vault
78
+ # x86_64:
79
+ # wget https://github.com/99designs/aws-vault/releases/latest/download/aws-vault-linux-amd64 -O aws-vault
80
+ chmod +x aws-vault && sudo mv aws-vault /usr/local/bin/
81
+
82
+ # 3. Install AWS CLI
83
+ # ARM64:
84
+ curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o awscliv2.zip
85
+ # x86_64:
86
+ # curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
87
+ unzip awscliv2.zip && sudo ./aws/install
88
+
89
+ # 4. Install Session Manager Plugin
90
+ # ARM64:
91
+ curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_arm64/session-manager-plugin.deb" -o session-manager-plugin.deb
92
+ # x86_64:
93
+ # curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o session-manager-plugin.deb
94
+ sudo dpkg -i session-manager-plugin.deb
95
+ ```
96
+
97
+ ### Windows
98
+
99
+ Download the `.msi` or `.exe` installer from [GitHub Releases](https://github.com/yarka-guru/connection_app/releases).
100
+
101
+ Prerequisites must be installed separately: [aws-vault](https://github.com/99designs/aws-vault), [AWS CLI](https://aws.amazon.com/cli/), [Session Manager Plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html).
102
+
103
+ ### CLI (all platforms)
36
104
 
37
105
  ```bash
38
106
  npm install -g rds_ssm_connect
@@ -141,7 +209,7 @@ src/
141
209
  ## Publishing
142
210
 
143
211
  - **npm**: Published automatically via GitHub Actions when a release is created
144
- - **Desktop**: Multi-platform builds (macOS ARM64/x64, Linux x64, Windows x64) via `tauri-action` on git tags
212
+ - **Desktop**: Multi-platform builds (macOS ARM64/x64, Linux ARM64/x64, Windows x64) via `tauri-action` on git tags
145
213
 
146
214
  ## License
147
215
 
package/connect.js CHANGED
@@ -3,13 +3,14 @@
3
3
  import { exec } from 'node:child_process'
4
4
  import { EventEmitter } from 'node:events'
5
5
  import fs from 'node:fs/promises'
6
+ import net from 'node:net'
6
7
  import os from 'node:os'
7
8
  import path from 'node:path'
8
9
  import { promisify } from 'node:util'
9
10
  import { PROJECT_CONFIGS } from './envPortMapping.js'
10
11
 
11
12
  // Package info for version checking
12
- const packageJson = { name: 'rds_ssm_connect', version: '1.6.2' }
13
+ const packageJson = { name: 'rds_ssm_connect', version: '1.7.6' }
13
14
 
14
15
  const execAsync = promisify(exec)
15
16
 
@@ -22,6 +23,9 @@ const RETRY_CONFIG = {
22
23
  BASTION_WAIT_RETRY_DELAY_MS: 15000,
23
24
  PORT_FORWARDING_MAX_RETRIES: 2,
24
25
  SSM_AGENT_READY_WAIT_MS: 10000,
26
+ KEEPALIVE_INTERVAL_MS: 4 * 60 * 1000,
27
+ AUTO_RECONNECT_MAX_RETRIES: 50,
28
+ AUTO_RECONNECT_DELAY_MS: 3000,
25
29
  }
26
30
 
27
31
  // Version check configuration
@@ -119,6 +123,22 @@ async function sleep(ms) {
119
123
  return new Promise((resolve) => setTimeout(resolve, ms))
120
124
  }
121
125
 
126
+ // Keepalive: periodic TCP ping through the tunnel to prevent SSM idle timeout.
127
+ // Each connection attempt generates traffic on the SSM WebSocket channel,
128
+ // resetting the server-side idle timer (default 20 min).
129
+ function startKeepalive(localPort) {
130
+ const timer = setInterval(() => {
131
+ const socket = new net.Socket()
132
+ socket.setTimeout(5000)
133
+ socket.connect(parseInt(localPort, 10), '127.0.0.1', () => {
134
+ socket.destroy()
135
+ })
136
+ socket.on('error', () => socket.destroy())
137
+ socket.on('timeout', () => socket.destroy())
138
+ }, RETRY_CONFIG.KEEPALIVE_INTERVAL_MS)
139
+ return () => clearInterval(timer)
140
+ }
141
+
122
142
  async function terminateBastionInstance(ENV, instanceId, region) {
123
143
  const terminateCommand = `aws-vault exec ${ENV} -- aws ec2 terminate-instances --region ${region} --instance-ids ${instanceId}`
124
144
  await runCommand(terminateCommand)
@@ -487,7 +507,9 @@ async function getProfilesForProjectKey(projectKey) {
487
507
  return getProfilesForProject(allProfiles, projectConfig, PROJECT_CONFIGS)
488
508
  }
489
509
 
490
- // Connect to RDS through bastion - returns connection info and control object
510
+ // Connect to RDS through bastion - returns connection info and control object.
511
+ // Includes keepalive (prevents SSM idle timeout) and auto-reconnect
512
+ // (transparently reconnects on the same port if session drops unexpectedly).
491
513
  async function connect(projectKey, profile, options = {}) {
492
514
  const projectConfig = PROJECT_CONFIGS[projectKey]
493
515
  if (!projectConfig) {
@@ -498,6 +520,9 @@ async function connect(projectKey, profile, options = {}) {
498
520
  // Use provided localPort or fall back to computed port from profile
499
521
  const localPort = options.localPort || getLocalPort(profile, projectConfig)
500
522
 
523
+ let manualDisconnect = false
524
+ let stopKeepalive = null
525
+
501
526
  // Emit status updates
502
527
  const emit = (event, data) => {
503
528
  ipcEmitter.emit(event, data)
@@ -510,12 +535,12 @@ async function connect(projectKey, profile, options = {}) {
510
535
  const credentials = await getConnectionCredentials(profile, projectConfig)
511
536
 
512
537
  emit('status', { message: 'Finding bastion instance...' })
513
- const instanceId = await findBastionInstance(profile, region)
538
+ let currentInstanceId = await findBastionInstance(profile, region)
514
539
 
515
540
  emit('status', { message: 'Getting RDS endpoint...' })
516
- const rdsEndpoint = await getRdsEndpoint(profile, projectConfig)
541
+ let currentRdsEndpoint = await getRdsEndpoint(profile, projectConfig)
517
542
 
518
- if (!rdsEndpoint || rdsEndpoint === 'None') {
543
+ if (!currentRdsEndpoint || currentRdsEndpoint === 'None') {
519
544
  throw new Error('Failed to find the RDS endpoint.')
520
545
  }
521
546
 
@@ -528,29 +553,104 @@ async function connect(projectKey, profile, options = {}) {
528
553
  username: credentials.username,
529
554
  password: credentials.password,
530
555
  database,
531
- rdsEndpoint,
532
- instanceId,
556
+ rdsEndpoint: currentRdsEndpoint,
557
+ instanceId: currentInstanceId,
533
558
  }
534
559
 
535
560
  emit('credentials', connectionInfo)
536
561
  emit('status', { message: 'Starting port forwarding...' })
537
562
 
538
- // Start port forwarding
539
- const portForwardingPromise = startPortForwardingWithConfig(
540
- profile,
541
- instanceId,
542
- rdsEndpoint,
543
- localPort,
544
- rdsPort,
545
- region,
546
- 0,
547
- RETRY_CONFIG.PORT_FORWARDING_MAX_RETRIES,
548
- )
563
+ // Auto-reconnect session management loop.
564
+ // Keeps the tunnel alive on the SAME local port. Only exits on
565
+ // manual disconnect or after exhausting reconnection attempts.
566
+ const portForwardingPromise = (async () => {
567
+ let reconnectCount = 0
568
+
569
+ while (!manualDisconnect) {
570
+ try {
571
+ stopKeepalive = startKeepalive(localPort)
572
+
573
+ await startPortForwardingWithConfig(
574
+ profile,
575
+ currentInstanceId,
576
+ currentRdsEndpoint,
577
+ localPort,
578
+ rdsPort,
579
+ region,
580
+ 0,
581
+ RETRY_CONFIG.PORT_FORWARDING_MAX_RETRIES,
582
+ )
583
+
584
+ // Session ended — clean up keepalive
585
+ stopKeepalive?.()
586
+ stopKeepalive = null
587
+
588
+ if (manualDisconnect) break
589
+
590
+ // Unexpected disconnect (idle timeout, network issue) — auto-reconnect
591
+ reconnectCount++
592
+ if (reconnectCount > RETRY_CONFIG.AUTO_RECONNECT_MAX_RETRIES) {
593
+ throw new Error('Maximum auto-reconnection attempts reached.')
594
+ }
595
+
596
+ emit('status', {
597
+ message: `Session ended. Reconnecting... (${reconnectCount})`,
598
+ })
599
+ await sleep(RETRY_CONFIG.AUTO_RECONNECT_DELAY_MS)
600
+
601
+ if (manualDisconnect) break
602
+
603
+ // Re-discover infrastructure (bastion may have been replaced by ASG)
604
+ emit('status', { message: 'Finding bastion instance...' })
605
+ currentInstanceId = await findBastionInstance(profile, region)
606
+
607
+ emit('status', { message: 'Getting RDS endpoint...' })
608
+ currentRdsEndpoint = await getRdsEndpoint(profile, projectConfig)
609
+
610
+ if (!currentRdsEndpoint || currentRdsEndpoint === 'None') {
611
+ throw new Error(
612
+ 'Failed to find the RDS endpoint during reconnection.',
613
+ )
614
+ }
615
+
616
+ emit('status', { message: 'Reconnecting port forwarding...' })
617
+ } catch (error) {
618
+ stopKeepalive?.()
619
+ stopKeepalive = null
620
+
621
+ if (manualDisconnect) break
622
+
623
+ reconnectCount++
624
+ if (reconnectCount > RETRY_CONFIG.AUTO_RECONNECT_MAX_RETRIES) {
625
+ throw error
626
+ }
627
+
628
+ emit('status', {
629
+ message: `Connection error. Retrying... (${reconnectCount}/${RETRY_CONFIG.AUTO_RECONNECT_MAX_RETRIES})`,
630
+ })
631
+ await sleep(RETRY_CONFIG.AUTO_RECONNECT_DELAY_MS * 2)
632
+
633
+ if (manualDisconnect) break
634
+
635
+ try {
636
+ currentInstanceId = await findBastionInstance(profile, region)
637
+ currentRdsEndpoint = await getRdsEndpoint(profile, projectConfig)
638
+ if (!currentRdsEndpoint || currentRdsEndpoint === 'None') {
639
+ throw new Error('Failed to find RDS endpoint')
640
+ }
641
+ } catch (_innerError) {
642
+ // Will retry on next loop iteration
643
+ }
644
+ }
645
+ }
646
+ })()
549
647
 
550
648
  // Return connection control object
551
649
  return {
552
650
  connectionInfo,
553
651
  disconnect: () => {
652
+ manualDisconnect = true
653
+ stopKeepalive?.()
554
654
  // Kill all active child processes
555
655
  activeChildProcesses.forEach((child) => {
556
656
  if (child && !child.killed) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rds_ssm_connect",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
package/src/App.svelte CHANGED
@@ -206,7 +206,7 @@ async function openUrl(url) {
206
206
  async function loadProfiles() {
207
207
  if (!selectedProject) return
208
208
  try {
209
- profiles = await invoke('listprofiles', { projectKey: selectedProject })
209
+ profiles = await invoke('list_profiles', { projectKey: selectedProject })
210
210
  selectedProfile = ''
211
211
  } catch (err) {
212
212
  errorMessage = `Failed to load profiles: ${err}`
@@ -3057,7 +3057,7 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
3057
3057
 
3058
3058
  [[package]]
3059
3059
  name = "rds-ssm-connect"
3060
- version = "1.7.4"
3060
+ version = "1.7.5"
3061
3061
  dependencies = [
3062
3062
  "reqwest 0.12.28",
3063
3063
  "semver",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rds-ssm-connect"
3
- version = "1.7.4"
3
+ version = "1.7.6"
4
4
  description = "Secure RDS connections through AWS SSM"
5
5
  authors = ["Iaroslav Pyrogov"]
6
6
  edition = "2024"
@@ -133,7 +133,7 @@ async fn ensure_sidecar(
133
133
  // Get current PATH and extend with common installation locations
134
134
  let current_path = std::env::var("PATH").unwrap_or_default();
135
135
  let extended_path = format!(
136
- "{}:/usr/local/bin:/opt/homebrew/bin:{}/.local/bin",
136
+ "{}:/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:{}/.local/bin",
137
137
  current_path,
138
138
  std::env::var("HOME").unwrap_or_default()
139
139
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "RDS SSM Connect",
4
- "version": "1.7.4",
4
+ "version": "1.7.6",
5
5
  "identifier": "com.rds-ssm-connect.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run dev:vite",