Skip to content

Commit 8874934

Browse files
committed
feature: ProxyJump - stable v2.1.0
1 parent 2f0d646 commit 8874934

19 files changed

Lines changed: 343 additions & 55 deletions

File tree

.github/workflows/scoop.yml

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
name: Update Scoop Bucket
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_run:
7+
workflows: ["Release"]
8+
types: [completed]
9+
workflow_dispatch:
10+
inputs:
11+
tag:
12+
description: "Release tag (e.g. v1.0.0)"
13+
required: true
14+
15+
jobs:
16+
scoop:
17+
if: github.server_url == 'https://github.com' && (github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success')
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Get release info
21+
id: release
22+
run: |
23+
TAG="${{ github.event.release.tag_name || github.event.inputs.tag || github.event.workflow_run.head_branch }}"
24+
VERSION="${TAG#v}"
25+
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
26+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
27+
28+
- name: Download Windows binary and compute checksum
29+
id: checksum
30+
run: |
31+
TAG="${{ steps.release.outputs.tag }}"
32+
BASE="https://github.com/madLinux7/dssh/releases/download/${TAG}"
33+
curl -sL "${BASE}/dssh.exe" -o dssh.exe
34+
if [ ! -s dssh.exe ]; then
35+
echo "Failed to download dssh.exe from ${BASE}"
36+
exit 1
37+
fi
38+
echo "sha256=$(sha256sum dssh.exe | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
39+
40+
- name: Checkout scoop-bucket
41+
uses: actions/checkout@v4
42+
with:
43+
repository: madLinux7/scoop-bucket
44+
token: ${{ secrets.SCOOP_TOKEN }}
45+
46+
- name: Update manifest
47+
run: |
48+
VERSION="${{ steps.release.outputs.version }}"
49+
TAG="${{ steps.release.outputs.tag }}"
50+
SHA="${{ steps.checksum.outputs.sha256 }}"
51+
URL="https://github.com/madLinux7/dssh/releases/download/${TAG}/dssh.exe"
52+
53+
cat > bucket/dssh.json << MANIFEST
54+
{
55+
"version": "${VERSION}",
56+
"description": "The only SSH connection manager you'll ever need. TUI & CLI. ssh_config, SQLite or both.",
57+
"homepage": "https://github.com/madLinux7/dssh",
58+
"license": "MIT",
59+
"architecture": {
60+
"64bit": {
61+
"url": "${URL}",
62+
"hash": "${SHA}"
63+
}
64+
},
65+
"bin": "dssh.exe",
66+
"checkver": "github",
67+
"autoupdate": {
68+
"architecture": {
69+
"64bit": {
70+
"url": "https://github.com/madLinux7/dssh/releases/download/v\$version/dssh.exe"
71+
}
72+
}
73+
}
74+
}
75+
MANIFEST
76+
77+
# Remove leading whitespace from heredoc
78+
sed -i 's/^ //' bucket/dssh.json
79+
80+
- name: Commit and push
81+
run: |
82+
git config user.name "github-actions[bot]"
83+
git config user.email "github-actions[bot]@users.noreply.github.com"
84+
git add bucket/dssh.json
85+
if git diff --cached --quiet; then
86+
echo "Manifest already up to date. Nothing to commit."
87+
else
88+
git commit -m "Update dssh to ${{ steps.release.outputs.version }}"
89+
git push
90+
fi

.github/workflows/winget.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Update WinGet Package
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
8+
jobs:
9+
winget:
10+
name: Update winget-pkgs manifest
11+
if: github.server_url == 'https://github.com'
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Update Package
15+
uses: michidk/winget-updater@v1
16+
with:
17+
komac-token: ${{ secrets.KOMAC_TOKEN }}
18+
identifier: madLinux.dssh
19+
repo: madLinux7/dssh
20+
url: "https://github.com/madLinux7/dssh/releases/download/v{VERSION}/dssh.exe"

README.md

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Store connections in **SQLite**, your **ssh_config** file, or **both** — your
1414

1515
Passwords are encrypted using a master passphrase (_you should consider using pubkeys only tho ;))_.
1616

17+
📚 **Full docs:** [dssh.grolmes.com](https://dssh.grolmes.com)
18+
1719
<p align="center">
1820
<img src="demo_tabs.gif" alt="dssh tab navigation"><br>
1921
<sub>TUI navigation demo</sub>
@@ -59,6 +61,8 @@ Passwords are encrypted using a master passphrase (_you should consider using pu
5961
- [From GitHub Releases](#from-github-releases)
6062
- [From source](#from-source)
6163
- [Build locally](#build-locally)
64+
- [Documentation](#documentation)
65+
- [Contributing](#contributing)
6266
- [Acknowledgements](#-acknowledgements-)
6367

6468
## Features
@@ -82,11 +86,27 @@ Also:
8286

8387
dssh is a thin wrapper around your system's `ssh` binary:
8488

89+
```text
90+
┌──────────────┐ read/write ┌──────────────────────┐
91+
│ dssh (CLI │◄───────────────►│ ~/.dssh/dssh.db │
92+
│ or TUI) │ │ • connections │
93+
└──────┬───────┘ │ • encrypted passes │
94+
│ └──────────────────────┘
95+
│ key auth: syscall.Exec → ssh (zero overhead)
96+
│ pw auth: fork ssh + SSH_ASKPASS script
97+
98+
┌──────────────┐
99+
│ ssh │────► remote host
100+
└──────────────┘
101+
```
102+
85103
- **Key auth**`syscall.Exec` replaces the dssh process with ssh (zero overhead, full terminal control)
86104
- **Password auth** — ssh runs as a child process with `SSH_ASKPASS` to supply the decrypted password (no `sshpass` needed)
87105
- **Data** — connections stored in SQLite (`~/.dssh/dssh.db`), your `ssh_config` file, or both
88106
- **Crypto** — AES-256-GCM encryption with Argon2id key derivation for stored passwords
89107

108+
More on the [security model](https://dssh.grolmes.com/guides/security/) and [configuration](https://dssh.grolmes.com/reference/config/) in the docs.
109+
90110
## Connection Modes
91111

92112
On first launch, dssh asks you to choose a connection mode:
@@ -119,7 +139,7 @@ Change your mode anytime with `dssh config`. View current settings with `dssh co
119139
| `dssh` | Launch interactive connection picker |
120140
| `dssh <name>` | Connect to a saved host by name |
121141
| `dssh <name> -- <args>` | Connect with extra args forwarded to ssh |
122-
| `dssh add [-p PORT] [-d DIR] <name> <target> [password]` | Save a new connection |
142+
| `dssh add [-p PORT] [-d DIR] [-J JUMP] <name> <target> [password]` | Save a new connection |
123143
| `dssh rm <name>` | Delete a saved connection |
124144
| `dssh list` / `dssh ls` | List all saved connections |
125145
| `dssh create` / `dssh new` | Interactive form to create a connection |
@@ -134,12 +154,13 @@ Change your mode anytime with `dssh config`. View current settings with `dssh co
134154

135155
| Key | Action |
136156
|---|---|
137-
| `Tab` / `Shift+Tab` | Switch between tabs |
138-
| `` / `` | Navigate lists |
157+
| `Tab` / `Shift+Tab` | Switch between tabs (always), or move between form fields (Create / Edit) |
158+
| `` / `` | Switch between tabs (Connect / Edit list / Delete always; Create / Edit forms when on an empty field, the Save button, or the Save-To toggle) |
159+
| `` / `` | Navigate lists / move between form fields |
139160
| `Enter` | Select / confirm |
140161
| `Ctrl+L` | Toggle SQLite / ssh_config list (both mode) |
141162
| `Ctrl+T` | Toggle key / password auth (create/edit) |
142-
| `ESC` / `Q` | Quit |
163+
| `ESC` / `Ctrl+C` | Quit |
143164

144165
### Quick start - Let's Go!
145166

@@ -178,6 +199,12 @@ dssh add myserver ssh://root@192.168.1.10:2222
178199
# Start in a specific remote directory
179200
dssh add myserver -d /var/www root@192.168.1.10
180201

202+
# Through a jump host (ProxyJump / ssh -J)
203+
dssh add db01 -J jumpuser@bastion.example.com dbadmin@10.0.1.50
204+
205+
# Through a chain of jump hosts
206+
dssh add db01 -J jump1.example.com,jump2.example.com dbadmin@10.0.1.50
207+
181208
# With password (will prompt for master passphrase)
182209
dssh add myserver root@192.168.1.10 'my-ssh-password'
183210
```
@@ -229,12 +256,12 @@ dssh ls # alias
229256
```
230257

231258
```
232-
NAME USER HOST PORT AUTH DIR
233-
mike-pulse-001 nomad 10.51.140.154 22 key -
234-
myserver root 192.168.1.10 22 password -
235-
rpg-server npc 192.168.188.7 22222 key /var/larp
236-
sharp-nexus-001 deploy 10.105.210.233 22 key -
237-
skylink root skylink.vps 22 key -
259+
NAME USER HOST PORT AUTH DIR JUMP
260+
mike-pulse-001 nomad 10.51.140.154 22 key - -
261+
myserver root 192.168.1.10 22 password - -
262+
rpg-server npc 192.168.188.7 22222 key /var/larp -
263+
sharp-nexus-001 deploy 10.105.210.233 22 key - jumpuser@bastion
264+
skylink root skylink.vps 22 key - -
238265
```
239266

240267
### Remove a connection (CLI)
@@ -369,6 +396,27 @@ make build
369396
make release
370397
```
371398

399+
## Documentation
400+
401+
Full docs live at **[dssh.grolmes.com](https://dssh.grolmes.com)**:
402+
403+
- [Get started](https://dssh.grolmes.com/getting-started/) — install, pick a storage mode, save your first connection
404+
- [Command reference](https://dssh.grolmes.com/reference/commands/) — every CLI flag and example
405+
- [TUI keybindings](https://dssh.grolmes.com/reference/tui-keys/) — key map per screen
406+
- [Configuration](https://dssh.grolmes.com/reference/config/) — parse modes, file locations, session flags
407+
- [Security model](https://dssh.grolmes.com/guides/security/) — crypto flow, threat model, key vs password
408+
- [Migration](https://dssh.grolmes.com/guides/migration/) — from `ssh_config`, across machines, between modes
409+
- [Troubleshooting](https://dssh.grolmes.com/guides/troubleshooting/) — permission denied, lost passphrase, WSL quirks
410+
- [FAQ](https://dssh.grolmes.com/guides/faq/) — YubiKey, passphrase rotation, portability
411+
- [Limitations](https://dssh.grolmes.com/reference/limitations/) — what dssh deliberately doesn't do
412+
413+
## Contributing
414+
415+
PRs welcome. See the [Contributing guide](https://dssh.grolmes.com/contributing/) for the dev loop, package layout, and PR checklist.
416+
417+
Bugs and feature requests: [github.com/madLinux7/dssh/issues](https://github.com/madLinux7/dssh/issues).
418+
Security disclosures: please use GitHub's [Security advisory](https://github.com/madLinux7/dssh/security/advisories/new) flow — don't file in the public tracker.
419+
372420
## ✨ Acknowledgements ✨
373421

374422
dssh has no need for reinventing the wheel — thanks to the maintainers and contributors of these amazing projects:

internal/cli/add.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ import (
1515
func newAddCmd() *cobra.Command {
1616
var port int
1717
var directory string
18+
var proxyJump string
1819
var addToSQLite bool
1920
var addToSSHConfig bool
2021

2122
cmd := &cobra.Command{
22-
Use: "add [-p PORT] [-d DIR] NAME target [password]",
23+
Use: "add [-p PORT] [-d DIR] [-J JUMP] NAME target [password]",
2324
Short: "Add a new SSH connection",
2425
Long: `Add a new SSH connection. Target can be user@host or ssh://user@host:port.
2526
If a password is provided, the connection uses password authentication
@@ -51,6 +52,7 @@ and the password is encrypted with your master passphrase.`,
5152
Host: host,
5253
Port: parsedPort,
5354
Directory: directory,
55+
ProxyJump: proxyJump,
5456
AuthType: model.AuthKey,
5557
}
5658

@@ -108,6 +110,7 @@ and the password is encrypted with your master passphrase.`,
108110
cmd.Flags().StringVarP(&directory, "directory", "d", "", "Remote directory to cd into on connect")
109111
cmd.Flags().StringVar(&directory, "cd", "", "Alias for --directory")
110112
cmd.Flags().MarkHidden("cd")
113+
cmd.Flags().StringVarP(&proxyJump, "proxy-jump", "J", "", "ProxyJump host (user@bastion / host1,host2)")
111114
cmd.Flags().BoolVar(&addToSQLite, "sqlite", false, "Save to SQLite")
112115
cmd.Flags().BoolVar(&addToSSHConfig, "sshconfig", false, "Save to ssh_config")
113116
return cmd

internal/cli/create.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func savePasswordAuth(d *sql.DB, wr *tui.WizardResult) error {
5454
Host: wr.Host,
5555
Port: port,
5656
Directory: wr.Directory,
57+
ProxyJump: wr.ProxyJump,
5758
AuthType: model.AuthPassword,
5859
}
5960

internal/cli/list.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,23 @@ func newListCmd() *cobra.Command {
3030

3131
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
3232
if showSource {
33-
fmt.Fprintln(w, "NAME\tUSER\tHOST\tPORT\tAUTH\tDIR\tSOURCE")
33+
fmt.Fprintln(w, "NAME\tUSER\tHOST\tPORT\tAUTH\tDIR\tJUMP\tSOURCE")
3434
} else {
35-
fmt.Fprintln(w, "NAME\tUSER\tHOST\tPORT\tAUTH\tDIR")
35+
fmt.Fprintln(w, "NAME\tUSER\tHOST\tPORT\tAUTH\tDIR\tJUMP")
3636
}
3737
for _, c := range conns {
3838
dir := c.Directory
3939
if dir == "" {
4040
dir = "-"
4141
}
42+
jump := c.ProxyJump
43+
if jump == "" {
44+
jump = "-"
45+
}
4246
if showSource {
43-
fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s\t%s\t%s\n", c.Name, c.User, c.Host, c.Port, c.AuthType, dir, c.Source)
47+
fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s\t%s\t%s\t%s\n", c.Name, c.User, c.Host, c.Port, c.AuthType, dir, jump, c.Source)
4448
} else {
45-
fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s\t%s\n", c.Name, c.User, c.Host, c.Port, c.AuthType, dir)
49+
fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s\t%s\t%s\n", c.Name, c.User, c.Host, c.Port, c.AuthType, dir, jump)
4650
}
4751
}
4852
return w.Flush()

internal/db/connections.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ func Insert(db *sql.DB, c *model.Connection) error {
2626
nonce = []byte{}
2727
}
2828
_, err := db.Exec(`
29-
INSERT INTO connections (name, user, host, port, directory, auth_type, identity_file, encrypted_pass, pass_nonce)
30-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
31-
c.Name, c.User, c.Host, c.Port, c.Directory, string(c.AuthType), c.IdentityFile, encPass, nonce,
29+
INSERT INTO connections (name, user, host, port, directory, auth_type, identity_file, proxy_jump, encrypted_pass, pass_nonce)
30+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
31+
c.Name, c.User, c.Host, c.Port, c.Directory, string(c.AuthType), c.IdentityFile, c.ProxyJump, encPass, nonce,
3232
)
3333
if err != nil {
3434
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
@@ -43,9 +43,9 @@ func Insert(db *sql.DB, c *model.Connection) error {
4343
func GetByName(db *sql.DB, name string) (*model.Connection, error) {
4444
c := &model.Connection{}
4545
err := db.QueryRow(`
46-
SELECT id, name, user, host, port, directory, auth_type, identity_file, encrypted_pass, pass_nonce, created_at, updated_at
46+
SELECT id, name, user, host, port, directory, auth_type, identity_file, proxy_jump, encrypted_pass, pass_nonce, created_at, updated_at
4747
FROM connections WHERE name = ?`, name,
48-
).Scan(&c.ID, &c.Name, &c.User, &c.Host, &c.Port, &c.Directory, &c.AuthType, &c.IdentityFile, &c.EncryptedPass, &c.PassNonce, &c.CreatedAt, &c.UpdatedAt)
48+
).Scan(&c.ID, &c.Name, &c.User, &c.Host, &c.Port, &c.Directory, &c.AuthType, &c.IdentityFile, &c.ProxyJump, &c.EncryptedPass, &c.PassNonce, &c.CreatedAt, &c.UpdatedAt)
4949
if errors.Is(err, sql.ErrNoRows) {
5050
return nil, fmt.Errorf("%w: %q", ErrNotFound, name)
5151
}
@@ -58,7 +58,7 @@ func GetByName(db *sql.DB, name string) (*model.Connection, error) {
5858
// List returns all connections ordered by name.
5959
func List(db *sql.DB) ([]model.Connection, error) {
6060
rows, err := db.Query(`
61-
SELECT id, name, user, host, port, directory, auth_type, identity_file, encrypted_pass, pass_nonce, created_at, updated_at
61+
SELECT id, name, user, host, port, directory, auth_type, identity_file, proxy_jump, encrypted_pass, pass_nonce, created_at, updated_at
6262
FROM connections ORDER BY name`)
6363
if err != nil {
6464
return nil, fmt.Errorf("list connections: %w", err)
@@ -68,7 +68,7 @@ func List(db *sql.DB) ([]model.Connection, error) {
6868
var conns []model.Connection
6969
for rows.Next() {
7070
var c model.Connection
71-
if err := rows.Scan(&c.ID, &c.Name, &c.User, &c.Host, &c.Port, &c.Directory, &c.AuthType, &c.IdentityFile, &c.EncryptedPass, &c.PassNonce, &c.CreatedAt, &c.UpdatedAt); err != nil {
71+
if err := rows.Scan(&c.ID, &c.Name, &c.User, &c.Host, &c.Port, &c.Directory, &c.AuthType, &c.IdentityFile, &c.ProxyJump, &c.EncryptedPass, &c.PassNonce, &c.CreatedAt, &c.UpdatedAt); err != nil {
7272
return nil, fmt.Errorf("scan connection: %w", err)
7373
}
7474
conns = append(conns, c)
@@ -89,10 +89,10 @@ func Update(db *sql.DB, c *model.Connection) error {
8989
_, err := db.Exec(`
9090
UPDATE connections
9191
SET name=?, user=?, host=?, port=?, directory=?, auth_type=?,
92-
identity_file=?, encrypted_pass=?, pass_nonce=?, updated_at=datetime('now')
92+
identity_file=?, proxy_jump=?, encrypted_pass=?, pass_nonce=?, updated_at=datetime('now')
9393
WHERE id=?`,
9494
c.Name, c.User, c.Host, c.Port, c.Directory, string(c.AuthType),
95-
c.IdentityFile, encPass, nonce, c.ID,
95+
c.IdentityFile, c.ProxyJump, encPass, nonce, c.ID,
9696
)
9797
if err != nil {
9898
if strings.Contains(err.Error(), "UNIQUE constraint failed") {

0 commit comments

Comments
 (0)