@hahahhh/sshx 0.0.6 → 0.0.7-rc.1
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/README.md +43 -2
- package/bin/sshx.js +7 -13
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
# sshx
|
|
2
2
|
|
|
3
|
+
**English** · [简体中文](README_zh.md) · [Website](https://xiaot623.github.io/sshx/)
|
|
4
|
+
|
|
3
5
|
> Transparent SSH enhancement — add remote-to-local commands, auto port forwarding, and local domains to your SSH workflow. Zero side effects when you don't need them.
|
|
4
6
|
|
|
5
7
|
**sshx** is a drop-in wrapper around OpenSSH. Wrap it as `alias ssh=sshx` and your existing SSH workflow works exactly as before — every flag, config, and connection passes through verbatim. But when you connect to a host (or Docker container) with sshx-aware features enabled, you unlock a connection-scoped shared remote server that gives you:
|
|
6
8
|
|
|
9
|
+

|
|
10
|
+
|
|
7
11
|
- 🔄 **Reverse command bridge** — run `sshx local <cmd>` *on the remote* to execute commands on your local machine, with stdout, stderr, exit code, and stdin all propagated.
|
|
8
12
|
- 📁 **Bidirectional workspace mount** — direct CLI sessions can use remote tools on local files, and local tools on remote files.
|
|
9
13
|
- 🔌 **Automatic port forwarding** — remote local listeners (loopback `127.0.0.1` and wildcard `0.0.0.0`; e.g., a dev server on `0.0.0.0:8080` or `localhost:8080`) are automatically detected and forwarded to your local machine.
|
|
10
14
|
- 🌐 **Local domain binding** — access forwarded ports as `<host>.<your-user>.sshx:<port>` in your local browser, no manual `-L` flags needed.
|
|
15
|
+
- 🛡️ **Remote egress proxy** — opt in to route proxy-aware remote tools through the local client's proxy or TUN-backed network stack.
|
|
11
16
|
- 🐳 **Docker container support** — target running containers by name or ID: `sshx my-container`. Command bridge support works inside containers via `docker exec`.
|
|
12
17
|
|
|
18
|
+
## Table of Contents
|
|
19
|
+
|
|
20
|
+
- [Why sshx?](#why-sshx)
|
|
21
|
+
- [Architecture](#architecture)
|
|
22
|
+
- [Features](#features)
|
|
23
|
+
- [Installation](#installation)
|
|
24
|
+
- [Quick Start](#quick-start)
|
|
25
|
+
- [Configuration Reference](#configuration-reference)
|
|
26
|
+
- [How It Works](#how-it-works)
|
|
27
|
+
- [Safety & Bypass](#safety--bypass)
|
|
28
|
+
- [Platform Support](#platform-support)
|
|
29
|
+
- [Project Structure](#project-structure)
|
|
30
|
+
- [Roadmap](#roadmap)
|
|
31
|
+
- [License](#license)
|
|
32
|
+
|
|
13
33
|
## Why sshx?
|
|
14
34
|
|
|
15
35
|
| Without sshx | With sshx |
|
|
@@ -80,7 +100,7 @@ Set `features.remoteFs: true` to expose the command initiator's workspace throug
|
|
|
80
100
|
- VS Code/Cursor Remote-SSH integration sidecars stay remote-to-local only: those application sessions do not export a local workspace to the remote host.
|
|
81
101
|
- With RemoteFS disabled, the command runs from the local home and receives `SSHX_REMOTE_CWD` plus `SSHX_REMOTE_FS=0`.
|
|
82
102
|
|
|
83
|
-
Mounted trees permit reads, writes, and creation
|
|
103
|
+
Mounted trees permit reads, writes, and creation. Deletion (unlink/rmdir) is blocked; same-directory rename is allowed so editors can atomic-save; cross-directory rename remains blocked. They can include sensitive files such as shell configuration and SSH credentials, so enable `remoteFs` only for targets you trust.
|
|
84
104
|
|
|
85
105
|
Set `FS_READ_ONLY=1` on the client when starting sshx to make the session mounts read-only:
|
|
86
106
|
|
|
@@ -144,6 +164,22 @@ ls /dev/macfuse*
|
|
|
144
164
|
|
|
145
165
|
Absolute source paths are preserved as a hierarchy below sshx's private session directory, but absolute command arguments are not rewritten. RemoteFS does not expose special files/xattrs/ACLs or support Docker targets, FUSE-T, or FSKit. It is optimized for source trees and small files rather than large-file throughput.
|
|
146
166
|
|
|
167
|
+
### 🛡️ Remote Egress Proxy (opt-in)
|
|
168
|
+
|
|
169
|
+
Set `features.proxy: true` or run with `SSHX_USE_PROXY=1` to give the remote session an OpenSSH remote dynamic SOCKS endpoint. Traffic exits the laptop:
|
|
170
|
+
|
|
171
|
+
```sh
|
|
172
|
+
SSHX_USE_PROXY=1 sshx remote
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
sshx asks the existing ControlMaster to allocate a remote SOCKS listener with `ssh -O forward -R 127.0.0.1:0` (no local destination). OpenSSH then acts as a SOCKS server on the remote, and connections leave through the client. sshx overrides uppercase and lowercase `HTTP_PROXY`, `HTTPS_PROXY`, and `ALL_PROXY` inside the remote session to `socks5h://127.0.0.1:<port>` so curl, Git, and other tools that only look at `HTTP_PROXY` still work. Existing `NO_PROXY` values are preserved and extended with remote loopback addresses.
|
|
176
|
+
|
|
177
|
+
There is no custom local HTTP/SOCKS process, no per-session credentials, and no `SSHX_PROXY_URL` chain-through. DNS names sent through `socks5h://` are resolved on the laptop. A local TUN proxy naturally captures that laptop egress.
|
|
178
|
+
|
|
179
|
+
The remote listener is bound only to `127.0.0.1` and uses a dynamically allocated port.
|
|
180
|
+
|
|
181
|
+
This feature covers TCP applications that honor proxy environment variables, including tools such as curl, Git, and many package managers. It does not provide a remote TUN device, UDP/ICMP forwarding, PAC/system-GUI proxy discovery, or Docker target support.
|
|
182
|
+
|
|
147
183
|
### 🔌 Automatic Port Detection & Forwarding
|
|
148
184
|
|
|
149
185
|
When a process on the remote starts listening on `127.0.0.1` or `0.0.0.0` (e.g., `npm run dev` on port 3000), sshx detects it and:
|
|
@@ -321,6 +357,10 @@ features:
|
|
|
321
357
|
# Requires FUSE on each machine receiving a mount.
|
|
322
358
|
remoteFs: false
|
|
323
359
|
|
|
360
|
+
# Route proxy-aware remote TCP applications through the local client.
|
|
361
|
+
# Default: false. SSHX_USE_PROXY=0|1 overrides this value.
|
|
362
|
+
proxy: false
|
|
363
|
+
|
|
324
364
|
commands:
|
|
325
365
|
# Commands blocked from bridge execution.
|
|
326
366
|
deny: []
|
|
@@ -347,7 +387,7 @@ commands:
|
|
|
347
387
|
1. **Connection**: `sshx remote` opens a normal SSH session and starts a compatible runtime under `~/.sshx_server/runtimes/<RuntimeHomeID>`.
|
|
348
388
|
2. **Sidecar channel**: One hidden SSH channel multiplexes command, port, heartbeat, and optional RemoteFS traffic. ContextID routes VS Code/Cursor terminals to a healthy live session.
|
|
349
389
|
3. **Port sniffing**: The server reads `/proc/net/tcp*` (Linux) to detect loopback (`127.0.0.1` / `::1`) and wildcard (`0.0.0.0` / `::`) listeners.
|
|
350
|
-
4. **Forwarding**: Detected ports are forwarded through a single shared local daemon using `ssh -
|
|
390
|
+
4. **Forwarding**: Detected ports are forwarded through a single shared local daemon using OpenSSH `ssh -O forward -L`.
|
|
351
391
|
5. **Domains**: The local DNS responder maps `<target>.<suffix>` → localhost. The browser's URL port selects the local forwarded port.
|
|
352
392
|
|
|
353
393
|
When `sshx` is invoked for a **non-matching host** (no sshx config, or host not in scope), it first checks if the target resolves to a running Docker container. If neither SSH nor Docker matches, it `exec`s the real `ssh` directly — no daemon, no installation, no overhead.
|
|
@@ -361,6 +401,7 @@ When `sshx` is invoked for a **non-matching host** (no sshx config, or host not
|
|
|
361
401
|
- `sshx local ...` on a **client** (not inside a remote session) — errors immediately with a clear message. `local` is globally reserved.
|
|
362
402
|
- `remoteFs` never silently falls back to an unmounted command. A failed FUSE mount fails the invocation.
|
|
363
403
|
- Remote exports are anchored with Go's `os.Root`; path traversal and symlink escapes are rejected.
|
|
404
|
+
- The remote egress proxy is OpenSSH remote dynamic SOCKS bound only to loopback.
|
|
364
405
|
- Docker containers that aren't running or can't be reached are pure passthrough — sshx falls back to raw `ssh` with no side effects.
|
|
365
406
|
- Unmatched hosts are pure passthrough — no files created, no processes started.
|
|
366
407
|
|
package/bin/sshx.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
rmSync,
|
|
10
10
|
writeFileSync,
|
|
11
11
|
} from "node:fs";
|
|
12
|
-
import { homedir
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
13
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
|
|
@@ -38,13 +38,11 @@ if (!platform || !arch) {
|
|
|
38
38
|
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
39
39
|
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
40
40
|
const version = packageJson.version;
|
|
41
|
-
const binaryName = `sshx-${platform}-${arch}
|
|
41
|
+
const binaryName = `sshx-${platform}-${arch}`;
|
|
42
42
|
const cacheRoot =
|
|
43
43
|
process.env.SSHX_CACHE_DIR ||
|
|
44
44
|
process.env.XDG_CACHE_HOME ||
|
|
45
|
-
(
|
|
46
|
-
? join(process.env.LOCALAPPDATA || tmpdir(), "sshx")
|
|
47
|
-
: join(homedir(), ".cache", "sshx"));
|
|
45
|
+
join(homedir(), ".cache", "sshx");
|
|
48
46
|
const binaryPath = join(cacheRoot, version, binaryName);
|
|
49
47
|
const refreshOnly =
|
|
50
48
|
process.argv.length === 3 && process.argv[2] === npmRefreshArgument;
|
|
@@ -95,7 +93,7 @@ function findNpmIntegrationsToRefresh() {
|
|
|
95
93
|
const descriptor = readIntegrationDescriptor(profileRoot);
|
|
96
94
|
const isMarked = existsSync(markerPath);
|
|
97
95
|
const isLegacyNpmIntegration =
|
|
98
|
-
descriptor !== null && pathIsInside(descriptor
|
|
96
|
+
descriptor !== null && pathIsInside(descriptor, cacheRoot);
|
|
99
97
|
|
|
100
98
|
if (!isMarked && !isLegacyNpmIntegration) {
|
|
101
99
|
continue;
|
|
@@ -103,7 +101,7 @@ function findNpmIntegrationsToRefresh() {
|
|
|
103
101
|
if (
|
|
104
102
|
isMarked &&
|
|
105
103
|
descriptor !== null &&
|
|
106
|
-
samePath(descriptor
|
|
104
|
+
samePath(descriptor, binaryPath)
|
|
107
105
|
) {
|
|
108
106
|
continue;
|
|
109
107
|
}
|
|
@@ -141,7 +139,7 @@ function readIntegrationDescriptor(profileRoot) {
|
|
|
141
139
|
readFileSync(join(profileRoot, "integration.json"), "utf8"),
|
|
142
140
|
);
|
|
143
141
|
if (typeof value.driverPath === "string" && value.driverPath.length > 0) {
|
|
144
|
-
return value;
|
|
142
|
+
return value.driverPath;
|
|
145
143
|
}
|
|
146
144
|
} catch {
|
|
147
145
|
// A marker without a readable descriptor is repaired by the native installer.
|
|
@@ -183,10 +181,6 @@ async function downloadBinary(destination) {
|
|
|
183
181
|
}
|
|
184
182
|
|
|
185
183
|
writeFileSync(tmpPath, Buffer.from(await response.arrayBuffer()));
|
|
186
|
-
|
|
187
|
-
if (platform !== "windows") {
|
|
188
|
-
chmodSync(tmpPath, 0o755);
|
|
189
|
-
}
|
|
190
|
-
|
|
184
|
+
chmodSync(tmpPath, 0o755);
|
|
191
185
|
renameSync(tmpPath, destination);
|
|
192
186
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hahahhh/sshx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7-rc.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Transparent SSH enhancement wrapper for OpenSSH",
|
|
6
6
|
"repository": {
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "node scripts/build-native.js",
|
|
24
24
|
"build:go": "go build -trimpath -ldflags \"-X github.com/xiaot623/sshx/internal/version.Version=${npm_package_version}\" -o sshx ./cmd/sshx",
|
|
25
|
+
"site": "npm --prefix docs install --no-audit --no-fund && npm --prefix docs run site",
|
|
26
|
+
"site:dev": "npm --prefix docs install --no-audit --no-fund && npm --prefix docs run dev",
|
|
27
|
+
"site:preview": "npm --prefix docs run preview",
|
|
25
28
|
"check": "go vet ./...",
|
|
26
29
|
"test": "go test ./... && node --test test/*.test.js",
|
|
27
30
|
"postinstall": "node ./bin/sshx.js --npm-refresh-integrations",
|