@hahahhh/sshx 0.0.7-rc.0 → 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 +7 -16
- package/bin/sshx.js +7 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ Set `features.remoteFs: true` to expose the command initiator's workspace throug
|
|
|
100
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.
|
|
101
101
|
- With RemoteFS disabled, the command runs from the local home and receives `SSHX_REMOTE_CWD` plus `SSHX_REMOTE_FS=0`.
|
|
102
102
|
|
|
103
|
-
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.
|
|
104
104
|
|
|
105
105
|
Set `FS_READ_ONLY=1` on the client when starting sshx to make the session mounts read-only:
|
|
106
106
|
|
|
@@ -166,25 +166,17 @@ Absolute source paths are preserved as a hierarchy below sshx's private session
|
|
|
166
166
|
|
|
167
167
|
### 🛡️ Remote Egress Proxy (opt-in)
|
|
168
168
|
|
|
169
|
-
Set `features.proxy: true` or run with `SSHX_USE_PROXY=1` to give the remote session
|
|
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
170
|
|
|
171
171
|
```sh
|
|
172
172
|
SSHX_USE_PROXY=1 sshx remote
|
|
173
|
-
|
|
174
|
-
# Optionally force a local upstream. Supported schemes (URL credentials work):
|
|
175
|
-
# http, https, socks5, socks5h
|
|
176
|
-
SSHX_USE_PROXY=1 SSHX_PROXY_URL=socks5h://127.0.0.1:7890 sshx remote
|
|
177
173
|
```
|
|
178
174
|
|
|
179
|
-
sshx overrides uppercase and lowercase `HTTP_PROXY`, `HTTPS_PROXY`, and `ALL_PROXY` inside the remote session. Existing `NO_PROXY` values are preserved and extended with remote loopback addresses.
|
|
180
|
-
|
|
181
|
-
Local egress selection is:
|
|
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.
|
|
182
176
|
|
|
183
|
-
|
|
184
|
-
2. For ordinary HTTP requests, `HTTP_PROXY` then `ALL_PROXY`; for HTTPS CONNECT and general SOCKS TCP connections, `ALL_PROXY`, `HTTPS_PROXY`, then `HTTP_PROXY`. Lowercase forms are also read, and local `NO_PROXY`/`no_proxy` rules bypass the selected upstream.
|
|
185
|
-
3. A normal local TCP connection. A local TUN proxy naturally captures this connection.
|
|
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.
|
|
186
178
|
|
|
187
|
-
The remote listener is bound only to `127.0.0.1
|
|
179
|
+
The remote listener is bound only to `127.0.0.1` and uses a dynamically allocated port.
|
|
188
180
|
|
|
189
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.
|
|
190
182
|
|
|
@@ -395,7 +387,7 @@ commands:
|
|
|
395
387
|
1. **Connection**: `sshx remote` opens a normal SSH session and starts a compatible runtime under `~/.sshx_server/runtimes/<RuntimeHomeID>`.
|
|
396
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.
|
|
397
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.
|
|
398
|
-
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`.
|
|
399
391
|
5. **Domains**: The local DNS responder maps `<target>.<suffix>` → localhost. The browser's URL port selects the local forwarded port.
|
|
400
392
|
|
|
401
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.
|
|
@@ -409,7 +401,7 @@ When `sshx` is invoked for a **non-matching host** (no sshx config, or host not
|
|
|
409
401
|
- `sshx local ...` on a **client** (not inside a remote session) — errors immediately with a clear message. `local` is globally reserved.
|
|
410
402
|
- `remoteFs` never silently falls back to an unmounted command. A failed FUSE mount fails the invocation.
|
|
411
403
|
- Remote exports are anchored with Go's `os.Root`; path traversal and symlink escapes are rejected.
|
|
412
|
-
- The remote egress proxy
|
|
404
|
+
- The remote egress proxy is OpenSSH remote dynamic SOCKS bound only to loopback.
|
|
413
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.
|
|
414
406
|
- Unmatched hosts are pure passthrough — no files created, no processes started.
|
|
415
407
|
|
|
@@ -444,7 +436,6 @@ sshx/
|
|
|
444
436
|
│ ├── remotefs/ # FS protocol, secure backend, and FUSE adapter
|
|
445
437
|
│ ├── ports/ # Port sniffing (/proc/net/tcp*)
|
|
446
438
|
│ ├── forward/ # TCP forwarding
|
|
447
|
-
│ ├── proxy/ # Authenticated local HTTP/SOCKS egress proxy
|
|
448
439
|
│ ├── domain/ # DNS resolver
|
|
449
440
|
│ └── locald/ # Local daemon (socket, DNS, forwarding)
|
|
450
441
|
├── scripts/ # Integration tests
|
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
|
}
|