ssh_tunnel_proxy 2.1.1 → 3.0.0
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/LICENSE +19 -3
- package/MIGRATION.md +227 -0
- package/README.md +360 -212
- package/build/bridge.d.ts +14 -0
- package/build/bridge.d.ts.map +1 -0
- package/build/bridge.js +12 -0
- package/build/bridge.js.map +1 -0
- package/build/connection.d.ts +60 -0
- package/build/connection.d.ts.map +1 -0
- package/build/connection.js +198 -0
- package/build/connection.js.map +1 -0
- package/build/credentials.d.ts +24 -0
- package/build/credentials.d.ts.map +1 -0
- package/build/credentials.js +102 -0
- package/build/credentials.js.map +1 -0
- package/build/errors.d.ts +19 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +64 -0
- package/build/errors.js.map +1 -0
- package/build/forward-spec.d.ts +34 -0
- package/build/forward-spec.d.ts.map +1 -0
- package/build/forward-spec.js +175 -0
- package/build/forward-spec.js.map +1 -0
- package/build/index.d.ts +19 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +71 -0
- package/build/index.js.map +1 -0
- package/build/legacy.d.ts +70 -0
- package/build/legacy.d.ts.map +1 -0
- package/build/legacy.js +188 -0
- package/build/legacy.js.map +1 -0
- package/build/local-forward.d.ts +31 -0
- package/build/local-forward.d.ts.map +1 -0
- package/build/local-forward.js +231 -0
- package/build/local-forward.js.map +1 -0
- package/build/model.d.ts +126 -0
- package/build/model.d.ts.map +1 -0
- package/build/model.js +55 -0
- package/build/model.js.map +1 -0
- package/build/port-policy.d.ts +43 -0
- package/build/port-policy.d.ts.map +1 -0
- package/build/port-policy.js +138 -0
- package/build/port-policy.js.map +1 -0
- package/build/remote-forward.d.ts +40 -0
- package/build/remote-forward.d.ts.map +1 -0
- package/build/remote-forward.js +226 -0
- package/build/remote-forward.js.map +1 -0
- package/build/tunnel.d.ts +121 -0
- package/build/tunnel.d.ts.map +1 -0
- package/build/tunnel.js +599 -0
- package/build/tunnel.js.map +1 -0
- package/examples/config.example.json +86 -0
- package/examples/setup_ssh.sh +12 -0
- package/examples/tunnel-cli.js +461 -0
- package/examples/tunnel-worker.js +98 -0
- package/examples/zrok-termux.ts +147 -0
- package/package.json +43 -53
- package/proto/sshtunnel/v1/tunnel.proto +343 -0
- package/dist/TestMessage.d.ts +0 -13
- package/dist/TestMessage.js +0 -32
- package/dist/TestMessage.js.map +0 -1
- package/dist/cjs/TestMessage.d.ts +0 -13
- package/dist/cjs/TestMessage.js +0 -32
- package/dist/cjs/TestMessage.js.map +0 -1
- package/dist/cjs/index.d.ts +0 -73
- package/dist/cjs/index.js +0 -621
- package/dist/cjs/index.js.map +0 -1
- package/dist/cjs/keypair_storage.d.ts +0 -12
- package/dist/cjs/keypair_storage.js +0 -143
- package/dist/cjs/keypair_storage.js.map +0 -1
- package/dist/cjs/ngrok_service.d.ts +0 -11
- package/dist/cjs/ngrok_service.js +0 -40
- package/dist/cjs/ngrok_service.js.map +0 -1
- package/dist/cjs/ssh2-node.d.ts +0 -1
- package/dist/cjs/ssh2-node.js +0 -211
- package/dist/cjs/ssh2-node.js.map +0 -1
- package/dist/esm/TestMessage.d.ts +0 -13
- package/dist/esm/TestMessage.js +0 -29
- package/dist/esm/TestMessage.js.map +0 -1
- package/dist/esm/index.d.ts +0 -73
- package/dist/esm/index.js +0 -616
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/keypair_storage.d.ts +0 -12
- package/dist/esm/keypair_storage.js +0 -117
- package/dist/esm/keypair_storage.js.map +0 -1
- package/dist/esm/ngrok_service.d.ts +0 -11
- package/dist/esm/ngrok_service.js +0 -37
- package/dist/esm/ngrok_service.js.map +0 -1
- package/dist/esm/ssh2-node.d.ts +0 -1
- package/dist/esm/ssh2-node.js +0 -184
- package/dist/esm/ssh2-node.js.map +0 -1
- package/dist/index.d.ts +0 -73
- package/dist/index.js +0 -620
- package/dist/index.js.map +0 -1
- package/dist/keypair_storage.d.ts +0 -12
- package/dist/keypair_storage.js +0 -120
- package/dist/keypair_storage.js.map +0 -1
- package/dist/ngrok_service.d.ts +0 -11
- package/dist/ngrok_service.js +0 -40
- package/dist/ngrok_service.js.map +0 -1
- package/dist/ssh2-node.d.ts +0 -1
- package/dist/ssh2-node.js +0 -188
- package/dist/ssh2-node.js.map +0 -1
- package/dist/types/TestMessage.d.ts +0 -13
- package/dist/types/index.d.ts +0 -73
- package/dist/types/keypair_storage.d.ts +0 -12
- package/dist/types/ngrok_service.d.ts +0 -11
- package/dist/types/ssh2-node.d.ts +0 -1
- package/ssh2-node +0 -2
package/README.md
CHANGED
|
@@ -1,267 +1,415 @@
|
|
|
1
1
|
# ssh_tunnel_proxy
|
|
2
|
-
Initiate a ssh reverse tunnel proxy with forwarding ports
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
SSH port forwarding for Node.js. You give it an SSH listener that already exists
|
|
4
|
+
at `host:port`. It keeps local (`ssh -L`) and reverse (`ssh -R`) forwards up over
|
|
5
|
+
that connection.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
```text
|
|
8
|
+
browser ──▶ 127.0.0.1:8280 ──▶ [ SSH ] ──▶ 127.0.0.1:8123 on the far side
|
|
9
|
+
```
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
The only runtime dependency is [`ssh2`](https://github.com/mscdex/ssh2).
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
## Why not `ssh -L`?
|
|
14
|
+
|
|
15
|
+
OpenSSH already does the same forwarding. Use it when a single hop stays up and
|
|
16
|
+
you are fine supervising the process yourself.
|
|
17
|
+
|
|
18
|
+
This package is for the hop that does not stay up — a phone, a NAT mapping, a
|
|
19
|
+
zrok or ngrok TCP share — and for code that has to *do something* when it drops.
|
|
15
20
|
|
|
21
|
+
| `ssh -L` / `ssh -R` | `ssh_tunnel_proxy` |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| A process you start and hope stays running | An object with `connect()`, `close()`, and events |
|
|
24
|
+
| Local port vanishes on drop (`ECONNREFUSED`, then a race to bind it again) | Listener stays bound in `degraded`; new clients fail fast; same port comes back |
|
|
25
|
+
| `ServerAliveInterval` or a restart loop in a shell | Bounded reconnect with backoff, or a hard stop at `maxAttempts` |
|
|
26
|
+
| stderr and exit codes | `TunnelError` with a stable `code` (`AUTH_FAILED`, `TARGET_UNREACHABLE`, …) |
|
|
27
|
+
| One shared stream per reverse forward in naive wrappers | One SSH channel and one TCP socket per connection |
|
|
28
|
+
| No picture of what is open | `getStatus()`, `listConnections()`, byte counts |
|
|
29
|
+
|
|
30
|
+
It does not replace `sshd`, generate keys, or create the relay. It is the layer
|
|
31
|
+
on top of an SSH listener that keeps forwards honest when the path is flaky.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install ssh_tunnel_proxy
|
|
16
37
|
```
|
|
17
|
-
|
|
38
|
+
|
|
39
|
+
The package includes `examples/tunnel-cli.js`. After `npm run build` (or from a
|
|
40
|
+
clone), you can bring forwards up with no application code.
|
|
41
|
+
|
|
42
|
+
## Quick start: the CLI
|
|
43
|
+
|
|
44
|
+
Copy `examples/config.example.json`, point each `transport.endpoint` at a host
|
|
45
|
+
that already speaks SSH, and give every service its own loopback listen port.
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
[
|
|
49
|
+
{
|
|
50
|
+
"name": "pi-living",
|
|
51
|
+
"transport": {
|
|
52
|
+
"endpoint": { "host": "192.168.1.21", "port": 22 },
|
|
53
|
+
"username": "user",
|
|
54
|
+
"privateKey": { "source": "file", "value": "~/.ssh/id_ed25519" }
|
|
55
|
+
},
|
|
56
|
+
"localForwards": [
|
|
57
|
+
{ "id": "ha", "listen": { "host": "127.0.0.1", "port": 8123 }, "target": { "host": "127.0.0.1", "port": 8123 } }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "pi-garage",
|
|
62
|
+
"transport": {
|
|
63
|
+
"endpoint": { "host": "192.168.1.22", "port": 22 },
|
|
64
|
+
"username": "user",
|
|
65
|
+
"privateKey": { "source": "file", "value": "~/.ssh/id_ed25519" }
|
|
66
|
+
},
|
|
67
|
+
"localForwards": [
|
|
68
|
+
{ "id": "cameras", "listen": { "host": "127.0.0.1", "port": 8080 }, "target": { "host": "127.0.0.1", "port": 8080 } }
|
|
69
|
+
],
|
|
70
|
+
"disabled": false
|
|
71
|
+
}
|
|
72
|
+
]
|
|
18
73
|
```
|
|
19
74
|
|
|
20
|
-
|
|
75
|
+
The example CLI starts one `worker_threads` Worker per config entry, so
|
|
76
|
+
each session has its own event loop. A blocked tick or a drop on one
|
|
77
|
+
board does not stall the others, and a local port stays bound while that
|
|
78
|
+
hop reconnects.
|
|
21
79
|
|
|
80
|
+
```bash
|
|
81
|
+
node examples/tunnel-cli.js --list -F ./config.json
|
|
82
|
+
node examples/tunnel-cli.js all -F ./config.json
|
|
22
83
|
```
|
|
23
|
-
|
|
84
|
+
|
|
85
|
+
`all` starts every entry that is not `"disabled": true`. Open
|
|
86
|
+
`http://127.0.0.1:8123` and `http://127.0.0.1:8080` in a browser on this
|
|
87
|
+
machine. Ctrl-C closes the lot.
|
|
88
|
+
|
|
89
|
+
A single host, or an override without editing the file:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
node examples/tunnel-cli.js pi-living -F ./config.json
|
|
93
|
+
node examples/tunnel-cli.js pi-living -F ./config.json -L 9090:127.0.0.1:3000
|
|
94
|
+
node examples/tunnel-cli.js user@192.168.1.21 -i ~/.ssh/id_ed25519 \
|
|
95
|
+
-L 8123:127.0.0.1:8123
|
|
24
96
|
```
|
|
25
97
|
|
|
26
|
-
|
|
27
|
-
|
|
98
|
+
Config path: `-F` / `--config`, else `$SSH_TUNNEL_PROXY_CONFIG`, else
|
|
99
|
+
`~/.config/ssh_tunnel_proxy/config.json`. v1 and v2 entry shapes are both
|
|
100
|
+
accepted. The CLI is an example on purpose — the library itself still takes a
|
|
101
|
+
`TunnelConfig` object and does not read a file.
|
|
102
|
+
|
|
103
|
+
Two config entries are two SSH sessions — and two Workers — even if they
|
|
104
|
+
point at the same host. That is how you isolate a filesystem mount from
|
|
105
|
+
a database on one VPS. Give each entry its own listen ports.
|
|
106
|
+
|
|
28
107
|
```json
|
|
29
|
-
[
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
108
|
+
[
|
|
109
|
+
{
|
|
110
|
+
"name": "vps-fs",
|
|
111
|
+
"transport": {
|
|
112
|
+
"endpoint": { "host": "host.example.net", "port": 22 },
|
|
113
|
+
"username": "user",
|
|
114
|
+
"privateKey": { "source": "file", "value": "~/.ssh/id_ed25519" }
|
|
115
|
+
},
|
|
116
|
+
"localForwards": [
|
|
117
|
+
{ "id": "sshd", "listen": { "host": "127.0.0.1", "port": 2222 }, "target": { "host": "127.0.0.1", "port": 22 } }
|
|
118
|
+
],
|
|
119
|
+
"portPolicy": { "allowedPrivilegedPorts": [22], "allowedRemoteHosts": ["127.0.0.1"] }
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"name": "vps-mariadb",
|
|
123
|
+
"transport": {
|
|
124
|
+
"endpoint": { "host": "host.example.net", "port": 22 },
|
|
125
|
+
"username": "user",
|
|
126
|
+
"privateKey": { "source": "file", "value": "~/.ssh/id_ed25519" }
|
|
127
|
+
},
|
|
128
|
+
"localForwards": [
|
|
129
|
+
{ "id": "mariadb", "listen": { "host": "127.0.0.1", "port": 13306 }, "target": { "host": "127.0.0.1", "port": 3306 } }
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
]
|
|
40
133
|
```
|
|
41
134
|
|
|
42
|
-
|
|
135
|
+
```bash
|
|
136
|
+
node examples/tunnel-cli.js all -F ./config.json
|
|
137
|
+
sshfs -p 2222 user@127.0.0.1:/var/www ~/mnt/vps -o reconnect
|
|
138
|
+
# MariaDB clients: 127.0.0.1:13306
|
|
43
139
|
```
|
|
44
|
-
|
|
140
|
+
|
|
141
|
+
A flap on the SFTP hop does not take the database session with it.
|
|
142
|
+
|
|
143
|
+
### Do not block the event loop
|
|
144
|
+
|
|
145
|
+
`ssh2` and this library run on Node’s single JS thread. A synchronous
|
|
146
|
+
`JSON.parse` of a large file, a tight CPU loop, or a giant
|
|
147
|
+
`readFileSync` in the **same process** pauses *every* tunnel in that
|
|
148
|
+
process: accepts, channel data, keepalives, and reconnect timers.
|
|
149
|
+
|
|
150
|
+
The example CLI is safe because it only reads a small config and then
|
|
151
|
+
moves bytes. If you embed `SSHTunnel` in an app that also parses,
|
|
152
|
+
indexes, or transforms large payloads:
|
|
153
|
+
|
|
154
|
+
- Prefer `worker_threads` (or a child process) for that work.
|
|
155
|
+
- Keep the process that calls `connect()` dedicated to SSH and local
|
|
156
|
+
sockets.
|
|
157
|
+
- Do not `JSON.parse` multi‑megabyte buffers on the tunnel tick.
|
|
158
|
+
|
|
159
|
+
The example CLI already puts each session on its own Worker. If you
|
|
160
|
+
embed `SSHTunnel` in a larger app, do the same — or use `worker_threads`
|
|
161
|
+
for parse/CPU and keep one dedicated Worker (or process) per transport.
|
|
162
|
+
|
|
163
|
+
## Quick start: the library
|
|
164
|
+
|
|
165
|
+
Same forwards from your own process, for example Home Assistant at
|
|
166
|
+
`127.0.0.1:8123`:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { SSHTunnel, credential } from 'ssh_tunnel_proxy';
|
|
170
|
+
|
|
171
|
+
const tunnel = new SSHTunnel({
|
|
172
|
+
transport: {
|
|
173
|
+
endpoint: { host: '127.0.0.1', port: 9191 },
|
|
174
|
+
username: 'user',
|
|
175
|
+
privateKey: credential.file('~/.ssh/id_ed25519'),
|
|
176
|
+
},
|
|
177
|
+
localForwards: [
|
|
178
|
+
{
|
|
179
|
+
id: 'home-assistant',
|
|
180
|
+
listen: { host: '127.0.0.1', port: 8123 },
|
|
181
|
+
target: { host: '127.0.0.1', port: 8123 },
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
tunnel.on('error', (err) => console.error(err.code, err.message));
|
|
187
|
+
await tunnel.connect();
|
|
188
|
+
|
|
189
|
+
const { endpoint } = tunnel.resolveEndpoint('home-assistant');
|
|
190
|
+
console.log(`open http://${endpoint.host}:${endpoint.port}`);
|
|
191
|
+
|
|
192
|
+
await tunnel.close();
|
|
45
193
|
```
|
|
46
194
|
|
|
47
|
-
|
|
195
|
+
`9191` is whatever local port your relay (zrok, ngrok, Tailscale, or a LAN
|
|
196
|
+
address) already bound. This package does not create that listener.
|
|
197
|
+
|
|
198
|
+
### Reverse forward
|
|
199
|
+
|
|
200
|
+
Expose a process on this machine to the peer (`ssh -R`):
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const status = await tunnel.addRemoteForward({
|
|
204
|
+
id: 'dev-server',
|
|
205
|
+
bind: { host: '127.0.0.1', port: 0 }, // 0 = peer chooses the port
|
|
206
|
+
target: { host: '127.0.0.1', port: 3000 },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
console.log('peer is listening on', status.assignedPort);
|
|
48
210
|
```
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
-2 --protocolv2 [protocolv2] Forces ssh to try protocol version 2 only.
|
|
60
|
-
-4 --ipv4only [ipv4only] Forces ssh to use IPv4 addresses only.
|
|
61
|
-
-6 --ipv6only [ipv6only] Forces ssh to use IPv6 addresses only.
|
|
62
|
-
-A --forward_enable [forward_enable] Enables forwarding of the authentication agent
|
|
63
|
-
connection.
|
|
64
|
-
-a --forward_disable [forward_disable] Disables forwarding of the authentication agent
|
|
65
|
-
connection.
|
|
66
|
-
-b --bind [bind] bind_address
|
|
67
|
-
-C --compress [compress] Requests compression of all data
|
|
68
|
-
-c --cipher [cipher] cipher_spec
|
|
69
|
-
-D --dynamic_forward [dynamic_forward] Specifies a local 'dynamic' application-level
|
|
70
|
-
port forwarding.
|
|
71
|
-
-e --escape [escape] escape_char Sets the escape character for
|
|
72
|
-
sessions with a pty.
|
|
73
|
-
-F --config [config] configfile
|
|
74
|
-
-f --background [background] Requests ssh to go to background just before
|
|
75
|
-
command execution.
|
|
76
|
-
-g --local_forward_remote [local_forward_remote] Allows remote hosts to connect to local forwarded
|
|
77
|
-
ports.
|
|
78
|
-
-I --smartcard [smartcard] smartcard_device Specify the device ssh.
|
|
79
|
-
-i --identity [identity] Private key filename.
|
|
80
|
-
-K --gssapi_auth [gssapi_auth] Enables GSSAPI-based authentication and
|
|
81
|
-
forwarding (delegation) of GSSAPI credentials to
|
|
82
|
-
the server.
|
|
83
|
-
-k --gssapi_disable [gssapi_disable] Disables forwarding (delegation) of GSSAPI
|
|
84
|
-
credentials to the server.
|
|
85
|
-
-L --local_forward_out [local_forward_out...] bind_address:port:host:hostport
|
|
86
|
-
-l --login_name [login_name] login_name
|
|
87
|
-
-M --master_mode [master_mode] Places the ssh client into 'master' mode for
|
|
88
|
-
connection sharing. Multiple -M options
|
|
89
|
-
-m --mac_spec [mac_spec] mac_spec
|
|
90
|
-
-N --no_exec [no_exec] Do not execute a remote command.
|
|
91
|
-
-n --redirect_stdin [redirect_stdin] Redirects stdin from /dev/null
|
|
92
|
-
-O --ctl_cmd [ctl_cmd] ctl_cmd
|
|
93
|
-
-o --option [option] option
|
|
94
|
-
-p --port [port] port Port to connect to on the remote host.
|
|
95
|
-
-q --quiet [quiet] Quiet mode. Causes most warning and diagnostic
|
|
96
|
-
messages to be suppressed.
|
|
97
|
-
-R --local_forward_in [local_forward_in...] bind_address:port:host:hostport
|
|
98
|
-
-S --ctl_path [ctl_path] ctl_path Specifies the location of a control
|
|
99
|
-
socket for connection sharing.
|
|
100
|
-
-s --subsystem [subsystem] May be used to request invocation of a subsystem
|
|
101
|
-
on the remote system.
|
|
102
|
-
-T --disable_tty [disable_tty] Disable pseudo-tty allocation.
|
|
103
|
-
-t --force_tty [force_tty] Force pseudo-tty allocation.
|
|
104
|
-
-V --version [version] Display the version number and exit.
|
|
105
|
-
-v --verbose [verbose] Verbose mode. Causes ssh to print debugging
|
|
106
|
-
messages about its progress.
|
|
107
|
-
-W --forward_stdin_stdout [forward_stdin_stdout] host:port Requests that standard input and output
|
|
108
|
-
on the client be forwarded to host on port ver
|
|
109
|
-
the secure channel.
|
|
110
|
-
-w --tunnel [tunnel] Requests tunnel device forwarding with the
|
|
111
|
-
specified tun(4) devices between the client
|
|
112
|
-
(local_tun) and the server (remote_tun).
|
|
113
|
-
-X --x11 [x11] Enables X11 forwarding.
|
|
114
|
-
-x --x11_disable [x11_disable] Disables X11 forwarding.
|
|
115
|
-
-Y --x11_trusted [x11_trusted] Enables trusted X11 forwarding.
|
|
116
|
-
-y --log [log] Send log information using the syslog(3)
|
|
117
|
-
-H --ngrok [ngrok] Obtain connectiion hostport from ngrok
|
|
118
|
-
-J --keychain_service [keychain_service] Service name to obtain private key from system
|
|
119
|
-
keychain.
|
|
120
|
-
-j --keychain_account [keychain_account] Account name to obtain private key from system
|
|
121
|
-
keychain.
|
|
122
|
-
-h, --help display help for command
|
|
211
|
+
|
|
212
|
+
Each inbound connection gets its own SSH channel and its own TCP socket.
|
|
213
|
+
|
|
214
|
+
### Several services on one transport
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
localForwards: [
|
|
218
|
+
{ id: 'home-assistant', listen: { host: '127.0.0.1', port: 8123 }, target: { host: '127.0.0.1', port: 8123 } },
|
|
219
|
+
{ id: 'cameras', listen: { host: '127.0.0.1', port: 8080 }, target: { host: '127.0.0.1', port: 8080 } },
|
|
220
|
+
]
|
|
123
221
|
```
|
|
124
|
-
### Api examples
|
|
125
222
|
|
|
126
|
-
|
|
223
|
+
Open `http://127.0.0.1:8123` and `http://127.0.0.1:8080` in a browser on this
|
|
224
|
+
machine. Listeners bind loopback only.
|
|
127
225
|
|
|
128
|
-
|
|
129
|
-
// send result of cmd through pipeline, generating a stream of json objects
|
|
130
|
-
function lsTest(cmdProxy, cmd, destination) {
|
|
226
|
+
## What this package does not do
|
|
131
227
|
|
|
132
|
-
|
|
228
|
+
See [MIGRATION.md](./MIGRATION.md) for the v1 mapping.
|
|
133
229
|
|
|
134
|
-
|
|
135
|
-
|
|
230
|
+
| Out of scope | Do this instead |
|
|
231
|
+
| --- | --- |
|
|
232
|
+
| Key generation and keychains | `credential.file`, `credential.env`, `credential.agent`, or `credential.callback` |
|
|
233
|
+
| Creating a zrok / ngrok / Tailscale share | Start the relay yourself, then pass `host:port` |
|
|
234
|
+
| Interactive shells | Use `ssh`, or `tunnel.exec()` for one command |
|
|
235
|
+
| Reading `~/.config/ssh_tunnel_proxy/config.json` | Pass a `TunnelConfig` object |
|
|
136
236
|
|
|
137
|
-
|
|
138
|
-
const pipelineReady = (socket) => {
|
|
237
|
+
## Using a TCP relay (zrok, ngrok, …)
|
|
139
238
|
|
|
140
|
-
|
|
239
|
+
1. On the host that runs `sshd` and the private apps, share the SSH port. With zrok:
|
|
141
240
|
|
|
142
|
-
|
|
143
|
-
|
|
241
|
+
```bash
|
|
242
|
+
zrok share private --backend-mode tcpTunnel 127.0.0.1:22
|
|
243
|
+
```
|
|
144
244
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
//self.cleanupPipeline(socket);
|
|
148
|
-
resolveCmd();
|
|
149
|
-
});
|
|
150
|
-
resolve();
|
|
151
|
-
})
|
|
152
|
-
}
|
|
245
|
+
2. On the machine that will run this library, bind that share locally and check
|
|
246
|
+
that it really is SSH:
|
|
153
247
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
destination,
|
|
159
|
-
pipelineReady
|
|
160
|
-
);
|
|
161
|
-
})
|
|
162
|
-
}
|
|
248
|
+
```bash
|
|
249
|
+
zrok access private --bind 127.0.0.1:9191 <share-token>
|
|
250
|
+
nc 127.0.0.1 9191 # must print SSH-2.0-...
|
|
251
|
+
```
|
|
163
252
|
|
|
164
|
-
|
|
253
|
+
3. Point `transport.endpoint` at `127.0.0.1:9191`.
|
|
165
254
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
255
|
+
A longer copy of this pattern is in `examples/zrok-termux.ts`.
|
|
256
|
+
|
|
257
|
+
## API
|
|
258
|
+
|
|
259
|
+
### `new SSHTunnel(config, options?)`
|
|
260
|
+
|
|
261
|
+
`TunnelConfig` matches `proto/sshtunnel/v1/tunnel.proto`.
|
|
262
|
+
|
|
263
|
+
| Method | Returns | Notes |
|
|
264
|
+
| --- | --- | --- |
|
|
265
|
+
| `connect()` | `Promise<TunnelStatus>` | Idempotent; concurrent calls share one attempt. |
|
|
266
|
+
| `close()` | `Promise<TunnelStatus>` | Idempotent. Stops reconnect and closes everything. |
|
|
267
|
+
| `addLocalForward(spec \| string)` | `Promise<ForwardStatus>` | Object or `"8280:127.0.0.1:8080"`. |
|
|
268
|
+
| `addLocalForwards(specs[])` | `Promise<ForwardStatus[]>` | Atomic: one bind failure rolls all of them back. |
|
|
269
|
+
| `addRemoteForward(spec \| string)` | `Promise<ForwardStatus>` | `bind.port: 0` → read `assignedPort`. |
|
|
270
|
+
| `addRemoteForwards(specs[])` | `Promise<ForwardStatus[]>` | Atomic, as above. |
|
|
271
|
+
| `removeForward(id)` | `Promise<ForwardStatus>` | Unbinds the listener and closes its connections. |
|
|
272
|
+
| `resolveEndpoint(id)` | `{ endpoint, state }` | Where to connect, including a peer-assigned port. |
|
|
273
|
+
| `getStatus()` | `TunnelStatus` | Tunnel + every forward. |
|
|
274
|
+
| `getForwardStatus(id)` | `ForwardStatus` | One forward. |
|
|
275
|
+
| `listConnections()` | `ConnectionStatus[]` | Live proxied connections and byte counts. |
|
|
276
|
+
| `getState()` / `isReady()` | `TunnelState` / `boolean` | |
|
|
277
|
+
| `exec(command, streams?)` | `Promise<{ code, stdout, stderr }>` | One command. No TTY. |
|
|
278
|
+
|
|
279
|
+
### Events
|
|
280
|
+
|
|
281
|
+
| Event | Payload | When |
|
|
282
|
+
| --- | --- | --- |
|
|
283
|
+
| `state` | `{ previous, current, reason? }` | Tunnel state change. |
|
|
284
|
+
| `ready` | `TunnelStatus` | Transport up and forwards open. Also after a successful reconnect. |
|
|
285
|
+
| `forward` | `{ status, previous }` | A forward changes state. |
|
|
286
|
+
| `connection` | `{ status, previous }` | One proxied connection changes state. |
|
|
287
|
+
| `reconnect` | `{ attempt, delayMs, cause? }` | A retry is scheduled. |
|
|
288
|
+
| `error` | `TunnelError` | Always has a `code`. |
|
|
289
|
+
| `close` | `TunnelStatus` | Reached `closed`. |
|
|
290
|
+
| `debug` | `(message, ...args)` | Tracing. `options.debugSsh` enables ssh2 protocol logs. |
|
|
291
|
+
|
|
292
|
+
### States
|
|
293
|
+
|
|
294
|
+
```text
|
|
295
|
+
tunnel idle → connecting → ready ⇄ reconnecting → closing → closed
|
|
296
|
+
↘ failed
|
|
297
|
+
|
|
298
|
+
forward pending → opening → active ⇄ degraded → closing → closed
|
|
299
|
+
↘ failed
|
|
300
|
+
|
|
301
|
+
connection accepted → opening → piped → half_closed → closed
|
|
302
|
+
↘ failed
|
|
303
|
+
```
|
|
172
304
|
|
|
173
|
-
|
|
305
|
+
`degraded`: the local listener stays bound while SSH is down. New clients fail
|
|
306
|
+
fast instead of getting `ECONNREFUSED` and racing to rebind. When SSH returns the
|
|
307
|
+
same forward becomes `active` again.
|
|
174
308
|
|
|
175
|
-
|
|
176
|
-
await sshTunnelProxy.connectSSH(opts);
|
|
309
|
+
### Errors
|
|
177
310
|
|
|
178
|
-
|
|
179
|
-
|
|
311
|
+
Branch on `err.code`, not on the message:
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
import { isTunnelError } from 'ssh_tunnel_proxy';
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
await tunnel.addLocalForward('80:127.0.0.1:8080');
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (isTunnelError(err) && err.code === 'PORT_NOT_PERMITTED') {
|
|
320
|
+
// privileged listen port was not allowlisted
|
|
321
|
+
}
|
|
180
322
|
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`INVALID_FORWARD_SPEC`, `PORT_NOT_PERMITTED`, `HOST_NOT_PERMITTED`,
|
|
326
|
+
`TRANSPORT_NOT_READY`, `AUTH_FAILED`, `LISTEN_FAILED`, `REMOTE_BIND_FAILED`,
|
|
327
|
+
`CHANNEL_OPEN_FAILED`, `TARGET_UNREACHABLE`, `DUPLICATE_FORWARD_ID`,
|
|
328
|
+
`UNKNOWN_FORWARD`, `CONNECTION_LIMIT`, `RECONNECT_EXHAUSTED`,
|
|
329
|
+
`CREDENTIAL_UNRESOLVED`.
|
|
181
330
|
|
|
182
|
-
|
|
331
|
+
## Credentials
|
|
332
|
+
|
|
333
|
+
The library only resolves a reference. It never writes a secret.
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
credential.file('~/.ssh/id_ed25519')
|
|
337
|
+
credential.env('SSH_PRIVATE_KEY')
|
|
338
|
+
credential.inline(pem)
|
|
339
|
+
credential.agent()
|
|
340
|
+
credential.callback('vault://key') // needs options.credentialResolver
|
|
183
341
|
```
|
|
184
342
|
|
|
185
|
-
|
|
343
|
+
Optional host-key check:
|
|
186
344
|
|
|
187
|
-
|
|
188
|
-
|
|
345
|
+
```ts
|
|
346
|
+
transport: {
|
|
347
|
+
// ...
|
|
348
|
+
hostKeyFingerprints: ['sha256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU'],
|
|
349
|
+
}
|
|
350
|
+
```
|
|
189
351
|
|
|
190
|
-
|
|
352
|
+
Omit fingerprints only on a path that is already authenticated (a private share).
|
|
191
353
|
|
|
192
|
-
|
|
193
|
-
const sshTunnelProxy = new SSHTunnelProxy();
|
|
194
|
-
|
|
195
|
-
ipcMain.on('connect_ssh_sync', async function(event,opts) {
|
|
196
|
-
// only allow connection on remote server to system ports http,https,ssh and user ports >1023
|
|
197
|
-
const portWhitelist = {
|
|
198
|
-
80:true,
|
|
199
|
-
443:true,
|
|
200
|
-
22:true
|
|
201
|
-
}
|
|
202
|
-
await sshTunnelProxy.connectSSH(opts, portWhitelist);
|
|
203
|
-
event.returnValue = 'connected';
|
|
204
|
-
});
|
|
354
|
+
## Port policy
|
|
205
355
|
|
|
206
|
-
|
|
356
|
+
Local listen ports and remote target ports are separate decisions. Local listeners
|
|
357
|
+
must be loopback (`127.0.0.1` or `::1`).
|
|
207
358
|
|
|
208
|
-
|
|
359
|
+
```ts
|
|
360
|
+
portPolicy: {
|
|
361
|
+
allowedPrivilegedPorts: [22, 80, 443],
|
|
362
|
+
allowedLocalListenPorts: [8123, 8080],
|
|
363
|
+
allowedRemoteTargetPorts: [8123, 8080, 22],
|
|
364
|
+
allowedRemoteHosts: ['127.0.0.1', '192.168.1.1'],
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Order: valid TCP port (remote bind may be `0`); ports below 1024 need
|
|
369
|
+
`allowedPrivilegedPorts`; a non-empty role list must include the port. No policy
|
|
370
|
+
means any unprivileged port, no privileged port.
|
|
209
371
|
|
|
210
|
-
|
|
372
|
+
## Data model
|
|
211
373
|
|
|
212
|
-
|
|
374
|
+
`proto/sshtunnel/v1/tunnel.proto` is the cross-language source of truth.
|
|
375
|
+
`src/model.ts` is a hand-written TypeScript projection; a unit test fails if they
|
|
376
|
+
drift. The runtime does not depend on protobuf.
|
|
213
377
|
|
|
214
|
-
|
|
378
|
+
## Migrating from v1
|
|
215
379
|
|
|
216
|
-
|
|
380
|
+
`SSHTunnelProxy` still exists:
|
|
217
381
|
|
|
218
|
-
|
|
382
|
+
```ts
|
|
383
|
+
import { SSHTunnelProxy } from 'ssh_tunnel_proxy';
|
|
219
384
|
|
|
220
|
-
|
|
385
|
+
const proxy = new SSHTunnelProxy();
|
|
386
|
+
proxy.on('ssh_tunnel_ready', () => console.log('up'));
|
|
387
|
+
await proxy.connectSSH(
|
|
388
|
+
{
|
|
389
|
+
username: 'user',
|
|
390
|
+
host: '127.0.0.1',
|
|
391
|
+
port: '9191',
|
|
392
|
+
private_key_filename: '~/.ssh/id_ed25519',
|
|
393
|
+
proxy_ports: ['8123:127.0.0.1:8123'],
|
|
394
|
+
},
|
|
395
|
+
{ 22: true, 80: true, 443: true },
|
|
396
|
+
);
|
|
221
397
|
```
|
|
222
398
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
network_offline: (...args) => ipcRenderer.send("network_offline", ...args),
|
|
233
|
-
ready: (callback) => ipcRenderer.on('ready', callback),
|
|
234
|
-
debug: (callback) => ipcRenderer.on('debug', callback),
|
|
235
|
-
error: (callback) => ipcRenderer.on('error', callback)
|
|
236
|
-
});
|
|
399
|
+
`ngrok_api`, `service_name`, and `shell` throw. Details in
|
|
400
|
+
[MIGRATION.md](./MIGRATION.md).
|
|
401
|
+
|
|
402
|
+
## Development
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
npm install
|
|
406
|
+
npm run build
|
|
407
|
+
npm test
|
|
237
408
|
```
|
|
238
409
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// initialize ssh message handlers
|
|
242
|
-
sshModule.ready((event, data) => {
|
|
243
|
-
});
|
|
244
|
-
sshModule.debug((event,msg)=>{
|
|
245
|
-
})
|
|
246
|
-
sshModule.error((event,err)=>{
|
|
247
|
-
});
|
|
410
|
+
Integration tests run an in-process `ssh2.Server`. No external `sshd` or network
|
|
411
|
+
is required.
|
|
248
412
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
"password": "",
|
|
253
|
-
"host": "",
|
|
254
|
-
"port": "",
|
|
255
|
-
"proxy_ports": [
|
|
256
|
-
"8280:127.0.0.1:80",
|
|
257
|
-
"9000:127.0.0.1:9000",
|
|
258
|
-
"8122:192.168.2.1:22"
|
|
259
|
-
],
|
|
260
|
-
"service_name": "service",
|
|
261
|
-
"server_name": "server",
|
|
262
|
-
"ngrok_api": "<ngrok_api_key>"
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
// initate ssh tunnel
|
|
266
|
-
sshModule.connect_ssh_sync(sshParams);
|
|
267
|
-
```
|
|
413
|
+
## License
|
|
414
|
+
|
|
415
|
+
MIT. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Client } from 'ssh2';
|
|
2
|
+
import type { TunnelError } from './errors';
|
|
3
|
+
import type { ConnectionState, ConnectionStatus, ForwardState, ForwardStatus } from './model';
|
|
4
|
+
export interface TransportBridge {
|
|
5
|
+
/** The live SSH client, or throw TRANSPORT_NOT_READY. */
|
|
6
|
+
requireClient(forwardId: string): Client;
|
|
7
|
+
/** The live SSH client, or undefined when the transport is down. */
|
|
8
|
+
peekClient(): Client | undefined;
|
|
9
|
+
onConnectionChanged(status: ConnectionStatus, previous: ConnectionState): void;
|
|
10
|
+
onForwardChanged(status: ForwardStatus, previous: ForwardState): void;
|
|
11
|
+
onError(error: TunnelError): void;
|
|
12
|
+
debug(message: string, ...args: unknown[]): void;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACd,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IACzC,oEAAoE;IACpE,UAAU,IAAI,MAAM,GAAG,SAAS,CAAC;IACjC,mBAAmB,CAAC,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;IAC/E,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IACtE,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAClC,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAClD"}
|
package/build/bridge.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* bridge.ts — the narrow surface a forward handle needs from its tunnel.
|
|
4
|
+
*
|
|
5
|
+
* Forward handles never reach into the tunnel, and the tunnel never reaches into
|
|
6
|
+
* a handle's sockets. Everything crosses this interface, which keeps the forward
|
|
7
|
+
* logic unit-testable against a fake transport.
|
|
8
|
+
*
|
|
9
|
+
* License: MIT
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG"}
|