@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.
Files changed (3) hide show
  1. package/README.md +43 -2
  2. package/bin/sshx.js +7 -13
  3. 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
+ ![sshx animated terminal demo](docs/assets/sshx-demo.svg)
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, but block file/directory deletion and rename in both directions. They can include sensitive files such as shell configuration and SSH credentials, so enable `remoteFs` only for targets you trust.
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 -W`.
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, tmpdir } from "node:os";
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}${platform === "windows" ? ".exe" : ""}`;
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
- (process.platform === "win32"
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.driverPath, cacheRoot);
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.driverPath, binaryPath)
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.6",
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",