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.
Files changed (109) hide show
  1. package/LICENSE +19 -3
  2. package/MIGRATION.md +227 -0
  3. package/README.md +360 -212
  4. package/build/bridge.d.ts +14 -0
  5. package/build/bridge.d.ts.map +1 -0
  6. package/build/bridge.js +12 -0
  7. package/build/bridge.js.map +1 -0
  8. package/build/connection.d.ts +60 -0
  9. package/build/connection.d.ts.map +1 -0
  10. package/build/connection.js +198 -0
  11. package/build/connection.js.map +1 -0
  12. package/build/credentials.d.ts +24 -0
  13. package/build/credentials.d.ts.map +1 -0
  14. package/build/credentials.js +102 -0
  15. package/build/credentials.js.map +1 -0
  16. package/build/errors.d.ts +19 -0
  17. package/build/errors.d.ts.map +1 -0
  18. package/build/errors.js +64 -0
  19. package/build/errors.js.map +1 -0
  20. package/build/forward-spec.d.ts +34 -0
  21. package/build/forward-spec.d.ts.map +1 -0
  22. package/build/forward-spec.js +175 -0
  23. package/build/forward-spec.js.map +1 -0
  24. package/build/index.d.ts +19 -0
  25. package/build/index.d.ts.map +1 -0
  26. package/build/index.js +71 -0
  27. package/build/index.js.map +1 -0
  28. package/build/legacy.d.ts +70 -0
  29. package/build/legacy.d.ts.map +1 -0
  30. package/build/legacy.js +188 -0
  31. package/build/legacy.js.map +1 -0
  32. package/build/local-forward.d.ts +31 -0
  33. package/build/local-forward.d.ts.map +1 -0
  34. package/build/local-forward.js +231 -0
  35. package/build/local-forward.js.map +1 -0
  36. package/build/model.d.ts +126 -0
  37. package/build/model.d.ts.map +1 -0
  38. package/build/model.js +55 -0
  39. package/build/model.js.map +1 -0
  40. package/build/port-policy.d.ts +43 -0
  41. package/build/port-policy.d.ts.map +1 -0
  42. package/build/port-policy.js +138 -0
  43. package/build/port-policy.js.map +1 -0
  44. package/build/remote-forward.d.ts +40 -0
  45. package/build/remote-forward.d.ts.map +1 -0
  46. package/build/remote-forward.js +226 -0
  47. package/build/remote-forward.js.map +1 -0
  48. package/build/tunnel.d.ts +121 -0
  49. package/build/tunnel.d.ts.map +1 -0
  50. package/build/tunnel.js +599 -0
  51. package/build/tunnel.js.map +1 -0
  52. package/examples/config.example.json +86 -0
  53. package/examples/setup_ssh.sh +12 -0
  54. package/examples/tunnel-cli.js +461 -0
  55. package/examples/tunnel-worker.js +98 -0
  56. package/examples/zrok-termux.ts +147 -0
  57. package/package.json +43 -53
  58. package/proto/sshtunnel/v1/tunnel.proto +343 -0
  59. package/dist/TestMessage.d.ts +0 -13
  60. package/dist/TestMessage.js +0 -32
  61. package/dist/TestMessage.js.map +0 -1
  62. package/dist/cjs/TestMessage.d.ts +0 -13
  63. package/dist/cjs/TestMessage.js +0 -32
  64. package/dist/cjs/TestMessage.js.map +0 -1
  65. package/dist/cjs/index.d.ts +0 -73
  66. package/dist/cjs/index.js +0 -621
  67. package/dist/cjs/index.js.map +0 -1
  68. package/dist/cjs/keypair_storage.d.ts +0 -12
  69. package/dist/cjs/keypair_storage.js +0 -143
  70. package/dist/cjs/keypair_storage.js.map +0 -1
  71. package/dist/cjs/ngrok_service.d.ts +0 -11
  72. package/dist/cjs/ngrok_service.js +0 -40
  73. package/dist/cjs/ngrok_service.js.map +0 -1
  74. package/dist/cjs/ssh2-node.d.ts +0 -1
  75. package/dist/cjs/ssh2-node.js +0 -211
  76. package/dist/cjs/ssh2-node.js.map +0 -1
  77. package/dist/esm/TestMessage.d.ts +0 -13
  78. package/dist/esm/TestMessage.js +0 -29
  79. package/dist/esm/TestMessage.js.map +0 -1
  80. package/dist/esm/index.d.ts +0 -73
  81. package/dist/esm/index.js +0 -616
  82. package/dist/esm/index.js.map +0 -1
  83. package/dist/esm/keypair_storage.d.ts +0 -12
  84. package/dist/esm/keypair_storage.js +0 -117
  85. package/dist/esm/keypair_storage.js.map +0 -1
  86. package/dist/esm/ngrok_service.d.ts +0 -11
  87. package/dist/esm/ngrok_service.js +0 -37
  88. package/dist/esm/ngrok_service.js.map +0 -1
  89. package/dist/esm/ssh2-node.d.ts +0 -1
  90. package/dist/esm/ssh2-node.js +0 -184
  91. package/dist/esm/ssh2-node.js.map +0 -1
  92. package/dist/index.d.ts +0 -73
  93. package/dist/index.js +0 -620
  94. package/dist/index.js.map +0 -1
  95. package/dist/keypair_storage.d.ts +0 -12
  96. package/dist/keypair_storage.js +0 -120
  97. package/dist/keypair_storage.js.map +0 -1
  98. package/dist/ngrok_service.d.ts +0 -11
  99. package/dist/ngrok_service.js +0 -40
  100. package/dist/ngrok_service.js.map +0 -1
  101. package/dist/ssh2-node.d.ts +0 -1
  102. package/dist/ssh2-node.js +0 -188
  103. package/dist/ssh2-node.js.map +0 -1
  104. package/dist/types/TestMessage.d.ts +0 -13
  105. package/dist/types/index.d.ts +0 -73
  106. package/dist/types/keypair_storage.d.ts +0 -12
  107. package/dist/types/ngrok_service.d.ts +0 -11
  108. package/dist/types/ssh2-node.d.ts +0 -1
  109. package/ssh2-node +0 -2
package/LICENSE CHANGED
@@ -1,5 +1,21 @@
1
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
1
+ MIT License
2
2
 
3
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
3
+ Copyright (c) 2023-2026 Autonomous
4
4
 
5
- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/MIGRATION.md ADDED
@@ -0,0 +1,227 @@
1
+ # Migrating from ssh_tunnel_proxy v1 to v2
2
+
3
+ v2 keeps the package name and keeps `ssh2` as the transport. What changed is scope:
4
+ the library now assumes **an SSH listener already exists at a host and port you
5
+ supply**, and concerns itself only with forwards over that transport.
6
+
7
+ Three things were removed outright, three things were added.
8
+
9
+ Removed: key generation and keychain storage, ngrok endpoint discovery, interactive
10
+ shells (plus the implicit read of `~/.config/ssh_tunnel_proxy/config.json`).
11
+
12
+ Added: a language-neutral `.proto` data model, a first-class `SSHTunnel` API with
13
+ typed events and per-connection state, and correct handling of many simultaneous
14
+ connections per forward.
15
+
16
+ ## The fastest path: keep using the shim
17
+
18
+ `SSHTunnelProxy` still exists. If you delete the removed options, most v1 programs
19
+ run unchanged:
20
+
21
+ ```diff
22
+ const proxy = new SSHTunnelProxy();
23
+ await proxy.connectSSH({
24
+ username: 'u0_a272',
25
+ host: '127.0.0.1',
26
+ port: '9191',
27
+ private_key_filename: '~/.ssh/id_ed25519',
28
+ proxy_ports: ['8280:127.0.0.1:8080'],
29
+ - ngrok_api: process.env.NGROK_API_KEY,
30
+ - service_name: 'sshtun',
31
+ - shell: false,
32
+ }, { 22: true, 80: true, 443: true });
33
+ ```
34
+
35
+ Leaving `ngrok_api`, `service_name` or `shell` in place throws immediately with a
36
+ message pointing here, rather than quietly doing something different from v1.
37
+
38
+ `proxy.getTunnel()` returns the v2 `SSHTunnel` behind the shim, so you can migrate
39
+ call sites one at a time.
40
+
41
+ ## Option mapping
42
+
43
+ | v1 option | v2 | Notes |
44
+ | --- | --- | --- |
45
+ | `username` | `transport.username` | |
46
+ | `host` / `hostname` | `transport.endpoint.host` | `host` wins; `hostname` is still accepted |
47
+ | `port` | `transport.endpoint.port` | Now a real number, validated. v1 used `parseInt`, so `"22abc"` became `22`; that now throws |
48
+ | `private_key_filename` | `transport.privateKey = credential.file(path)` | `~` still expands |
49
+ | `private_key` | `transport.privateKey = credential.inline(pem)` | |
50
+ | `password` | `transport.password = credential.inline(...)` | An empty string is now dropped rather than attempted |
51
+ | `proxy_ports` | `localForwards` | The v1 string form still works everywhere a `LocalForward` is accepted |
52
+ | `remote_ports` | `remoteForwards` | **Field order changed — see below** |
53
+ | `whitelist` | `portPolicy` | `whitelistToPortPolicy()` converts a v1 whitelist object |
54
+ | `keepaliveInterval` | `transport.keepaliveIntervalMs` | Same default of 10000 |
55
+ | `exec` | `exec` on the shim, or `tunnel.exec(cmd)` | Still runs after connect on the shim |
56
+ | `server_name` | *gone* | It only existed to name a keychain entry |
57
+ | `service_name` | *gone* | Keychain storage removed |
58
+ | `ngrok_api` | *gone* | Resolve the address yourself and pass `host:port` |
59
+ | `shell` | *gone* | Not port forwarding |
60
+ | `disabled` | *gone* | Do not construct the tunnel |
61
+
62
+ ## Breaking change to watch: reverse-forward spec strings
63
+
64
+ This is the one silent behaviour change, so check it if you used `remote_ports`.
65
+
66
+ ```text
67
+ v1 "9090:127.0.0.1:3000" → localPort : targetHost : remotePort
68
+ v2 "9090:127.0.0.1:3000" → bindPort : targetHost : targetPort
69
+ ```
70
+
71
+ v2 reads a three-field reverse spec the way `ssh -R` does: the **first** field is the
72
+ port the peer binds, and the last field is the port on **this** side that traffic is
73
+ delivered to. If your v1 values had the two ports the same — which is by far the
74
+ most common case — nothing changes. If they differed, swap them.
75
+
76
+ The unambiguous form is the object:
77
+
78
+ ```ts
79
+ await tunnel.addRemoteForward({
80
+ bind: { host: '127.0.0.1', port: 9090 }, // on the peer
81
+ target: { host: '127.0.0.1', port: 3000 }, // reachable from here
82
+ });
83
+ ```
84
+
85
+ Use `bind.port: 0` to let the peer assign a port, then read `assignedPort` from the
86
+ returned status. v1 had no way to express that.
87
+
88
+ ## The `ssh2-node <alias>` command line
89
+
90
+ v1 shipped a `commander`-based CLI that read
91
+ `~/.config/ssh_tunnel_proxy/config.json`, matched an alias against each entry's
92
+ `hostname`, and connected — usually dropping you into a remote shell.
93
+
94
+ The v2 equivalent is `examples/tunnel-cli.js`, which is dependency-free plain
95
+ JavaScript you are meant to copy and edit:
96
+
97
+ ```bash
98
+ node examples/tunnel-cli.js rh2 # same alias, same config file
99
+ node examples/tunnel-cli.js --list # new: see what is in the file
100
+ node examples/tunnel-cli.js rh2 -- uname -a
101
+ ```
102
+
103
+ It reads the same file from the same default path, and accepts v1-shaped entries
104
+ directly. Differences:
105
+
106
+ | v1 CLI | v2 example CLI |
107
+ | --- | --- |
108
+ | `ssh2-node rh2` | `node examples/tunnel-cli.js rh2` |
109
+ | `-L` / `-R` | `-L` / `-R`, same repeatable form (mind the `-R` field order above) |
110
+ | `-i`, `-p`, `-v` | Same, plus `-u` and `--debug-ssh` |
111
+ | No argument after the host → remote shell | Holds the tunnel open instead; use `ssh` through the forward |
112
+ | Trailing command → exec then exit | `-- command ...` → exec then exit with its status |
113
+ | `-J` / `-j` keychain lookup | Gone. Use `-i <path>` or `-i agent` |
114
+ | `-H` ngrok lookup | Gone. Put the resolved address in the entry |
115
+ | Iterated *all* matching config entries | Selects exactly one entry, and says so if the alias is missing |
116
+ | ~35 mostly unimplemented `ssh(1)` flags | Only the flags that do something |
117
+ | Config read inside the library | Config read by the CLI and passed in as an object |
118
+
119
+ That last row is the point: the library no longer reads any file, so the same code
120
+ behaves identically on your workstation, in a container, and in the test suite.
121
+ Stale keys such as `service_name` or `ngrok_api` in an existing config file produce a
122
+ warning from the CLI and are skipped, so your file keeps working while you clean it
123
+ up.
124
+
125
+ ## Method mapping
126
+
127
+ | v1 | v2 |
128
+ | --- | --- |
129
+ | `new SSHTunnelProxy()` + `connectSSH(opts, whitelist)` | `new SSHTunnel(config)` + `connect()` |
130
+ | `setupProxyPorts(ports)` | `addLocalForwards(specs)` — now atomic, and returns statuses |
131
+ | `setupRemotePorts(ports)` | `addRemoteForwards(specs)` |
132
+ | *(no equivalent)* | `removeForward(id)` |
133
+ | *(no equivalent)* | `resolveEndpoint(id)`, `getForwardStatus(id)`, `listConnections()` |
134
+ | `execCmd(cmd, out, err)` | `exec(cmd, { stdout, stderr })` — resolves with `{ code, stdout, stderr }` |
135
+ | `getClient()` | `getClient()` on the shim; v2 code should not need the raw client |
136
+ | `generateAndStoreKeypair()` | *gone* — use `ssh-keygen` |
137
+ | `getPublicKey()` | *gone* |
138
+ | `onNetworkOnline()` / `onNetworkOffline()` | *gone*; no-ops on the shim. Reconnection is now automatic and driven by the transport itself |
139
+ | `validate_port_number(port, whitelist)` | `checkPort(port, role, policy)` — the shim keeps the old boolean form |
140
+ | `validate_local_forward(ports, whitelist)` | `parseAndValidateLocalForward(spec, policy)` |
141
+ | `do_ssh_connect(...)` | *gone* (was internal) |
142
+
143
+ ## Events
144
+
145
+ | v1 event | v2 |
146
+ | --- | --- |
147
+ | `ssh_tunnel_ready` | `ready`, carrying a full `TunnelStatus`. The shim still emits `ssh_tunnel_ready`, now exactly once per successful connect |
148
+ | `status` | `state` (tunnel), `forward` (per forward), `connection` (per connection). The shim still emits `status` |
149
+ | `debug` | `debug` |
150
+ | `error` | `error`, always a `TunnelError` with a `code` |
151
+ | *(none)* | `reconnect`, `close` |
152
+
153
+ `error` is worth re-reading: in v1, some failures were logged and others were thrown
154
+ from deep inside a callback. In v2 anything non-fatal is emitted as a coded
155
+ `TunnelError`, and only the operation you called rejects.
156
+
157
+ ## Behaviour differences you may actually notice
158
+
159
+ 1. **Concurrent reverse-forward connections work.** v1 reused a single outbound
160
+ socket for every channel of a reverse forward, so a second simultaneous
161
+ connection interleaved with the first. v2 opens one socket per channel. There is a
162
+ test that fails if this regresses.
163
+
164
+ 2. **Port validation is stricter.** `parseInt`-style values such as `"80abc"` and
165
+ `"22.5"` are rejected instead of truncated.
166
+
167
+ 3. **Port policy is explicit and role-aware.** v1 applied one whitelist to
168
+ everything. v2 asks separately about local listen ports, remote target ports,
169
+ remote bind ports and remote hosts. With no policy supplied, any unprivileged port
170
+ is permitted and no privileged port is.
171
+
172
+ 4. **A dropped transport degrades rather than disappears.** Local listeners stay
173
+ bound while reconnection is in progress, so callers get a fast failure instead of
174
+ connection-refused, and the port cannot be stolen by another process in the gap.
175
+ Forwards return to `active` automatically.
176
+
177
+ 5. **Adding several forwards is atomic.** If the third of three fails to bind, the
178
+ first two are rolled back, so you never end up half-configured.
179
+
180
+ 6. **Nothing is read from disk implicitly.** v1 read
181
+ `~/.config/ssh_tunnel_proxy/config.json`; v2 only uses the config object you pass.
182
+ This is also why the test suite no longer depends on the machine it runs on.
183
+
184
+ 7. **Host keys can be verified.** Set `transport.hostKeyFingerprints` to one or more
185
+ `sha256:...` values. Omitting it accepts any host key, as v1 always did.
186
+
187
+ ## Where key management went
188
+
189
+ Out of the package. Choose whichever fits your deployment:
190
+
191
+ ```ts
192
+ credential.file('~/.ssh/id_ed25519') // ssh-keygen, file permissions, done
193
+ credential.env('SSH_PRIVATE_KEY') // containers and CI
194
+ credential.agent() // ssh-agent, no key bytes in your process
195
+ credential.callback('vault://ssh/id') // your own resolver, e.g. Vault or an OS keychain
196
+ ```
197
+
198
+ `credential.callback` plus `options.credentialResolver` is the seam where the old
199
+ `keytar` behaviour belongs if you still want it — as ten lines in your application,
200
+ not as a native dependency of a forwarding library.
201
+
202
+ ## Where ngrok went
203
+
204
+ Also out of the package. v1 called the ngrok API to discover where the SSH daemon
205
+ was, which coupled the transport to one vendor. v2 takes an address:
206
+
207
+ ```ts
208
+ // zrok: `zrok access private --bind 127.0.0.1:9191 <token>` on this machine
209
+ endpoint: { host: '127.0.0.1', port: 9191 }
210
+
211
+ // ngrok: read the address from the ngrok API in your own code, then
212
+ endpoint: { host: '4.tcp.ngrok.io', port: 12345 }
213
+
214
+ // Tailscale, WireGuard, LAN, jump host — all just an address
215
+ endpoint: { host: '100.64.0.7', port: 22 }
216
+ ```
217
+
218
+ `transport.reachability` (`'direct' | 'zrok' | 'ngrok' | 'custom'`) exists only to
219
+ label how you got there. It changes no behaviour.
220
+
221
+ ## Cross-language use
222
+
223
+ `proto/sshtunnel/v1/tunnel.proto` defines the config, status and event model plus an
224
+ optional `TunnelControl` gRPC service, so another runtime can drive or observe the
225
+ same model. The Node runtime has no protobuf dependency and no codegen step; the
226
+ proto file is the specification, and `test/unit/model-parity.test.js` fails the build
227
+ if `src/model.ts` drifts from it.