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.
- package/.github/workflows/release.yml +0 -4
- package/README.md +77 -9
- package/connect.js +118 -18
- package/package.json +1 -1
- package/src/App.svelte +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/lib.rs +1 -1
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -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
|
-
###
|
|
25
|
+
### macOS (Homebrew)
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
```bash
|
|
28
|
+
brew tap yarka-guru/tap
|
|
29
|
+
brew install --cask rds-ssm-connect
|
|
30
|
+
```
|
|
28
31
|
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
538
|
+
let currentInstanceId = await findBastionInstance(profile, region)
|
|
514
539
|
|
|
515
540
|
emit('status', { message: 'Getting RDS endpoint...' })
|
|
516
|
-
|
|
541
|
+
let currentRdsEndpoint = await getRdsEndpoint(profile, projectConfig)
|
|
517
542
|
|
|
518
|
-
if (!
|
|
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
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
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('
|
|
209
|
+
profiles = await invoke('list_profiles', { projectKey: selectedProject })
|
|
210
210
|
selectedProfile = ''
|
|
211
211
|
} catch (err) {
|
|
212
212
|
errorMessage = `Failed to load profiles: ${err}`
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
package/src-tauri/src/lib.rs
CHANGED
|
@@ -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
|
);
|