tunnel-rb 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +506 -0
- data/exe/tunnel +6 -0
- data/lib/tunnel_rb/cli.rb +59 -0
- data/lib/tunnel_rb/client.rb +195 -0
- data/lib/tunnel_rb/server/client.rb +29 -0
- data/lib/tunnel_rb/server/client_registry.rb +96 -0
- data/lib/tunnel_rb/server/control_server.rb +244 -0
- data/lib/tunnel_rb/server/http_request.rb +45 -0
- data/lib/tunnel_rb/server/logger.rb +23 -0
- data/lib/tunnel_rb/server/pending_connections.rb +43 -0
- data/lib/tunnel_rb/server/public_server.rb +144 -0
- data/lib/tunnel_rb/server/socket_helpers.rb +41 -0
- data/lib/tunnel_rb/server/thread_pool.rb +39 -0
- data/lib/tunnel_rb/server/tls.rb +48 -0
- data/lib/tunnel_rb/server/token_store.rb +182 -0
- data/lib/tunnel_rb/server.rb +96 -0
- data/lib/tunnel_rb/version.rb +5 -0
- data/lib/tunnel_rb.rb +8 -0
- data/tunnel-rb.gemspec +35 -0
- metadata +92 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7f05bc6a5b62aa9876fe9214d1b45d8bffa70f307805bb9cedda03c5aefbc3c9
|
|
4
|
+
data.tar.gz: b7a6fca4bbaddd1d6f18f2ad5a493f1f7e4f497bdace5879717e25fc20149720
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: add440acc45cc5a459a54b7c6137a7d254faa34430b44706519b2b46b532bd9b247ab60112452722be3de13cfe83eb94e21b8efcf48853c184350a62cf88e6b5
|
|
7
|
+
data.tar.gz: 0f88caa949e07e68eb73371f5eedd2c0cf4c3eaaf1d6b8b24e60b4db60161d7a87d70e4a88479dac347ec9e2294bdb4a51eda8b24d4e3e1d7807cd923351d7c4
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) tunnel-rb contributors
|
|
4
|
+
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# [tunnel-rb](https://github.com/headmandev/tunnel-rb)
|
|
2
|
+
|
|
3
|
+
Expose a local HTTP server (e.g. a Rails app on port 3000) to the internet through a tunnel server. A tunnel **client** running on your machine maintains a persistent control connection to the tunnel **server**; incoming browser traffic hits the server and is forwarded through the client to your local process.
|
|
4
|
+
|
|
5
|
+
**By default, no server setup is required.** The client connects to the hosted tunnel server at [tunnel-rb.dev](https://tunnel-rb.dev) and prints a public HTTPS URL you can open or share. Run your own server only if you want to self-host a private instance.
|
|
6
|
+
|
|
7
|
+
Built with plain Ruby — blocking I/O and threads, no runtime gem dependencies.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
gem install tunnel-rb
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This installs the `tunnel` client on your `PATH`:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
tunnel 3000
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
From source:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/headmandev/tunnel-rb.git
|
|
25
|
+
cd tunnel-rb
|
|
26
|
+
bundle install
|
|
27
|
+
bundle exec rake install # or: gem build tunnel-rb.gemspec && gem install tunnel-rb-*.gem
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Without installing the gem (from a checkout):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
ruby tunnel.rb 3000
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The root-level script `tunnel.rb` (and `bin/tunnel`) remain for development checkout.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
Start your local app, then run the client with its port — no `--server-host` or other server options needed:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
rails server -p 3000 # or any local HTTP server
|
|
44
|
+
|
|
45
|
+
tunnel 3000
|
|
46
|
+
# from checkout: ruby tunnel.rb 3000
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The client connects to the hosted server at **tunnel-rb.dev** (control host: `server.tunnel-rb.dev`) and prints a public URL:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
🚀 [Tunnel] Ready! https://1ef59cf5.tunnel-rb.dev -> localhost:3000 (server: TLS)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Open that URL in a browser, or:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
curl -I https://1ef59cf5.tunnel-rb.dev/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Each run gets a unique subdomain. TLS to the server is on by default.
|
|
62
|
+
|
|
63
|
+
> **Self-hosting:** Run your own server with `ruby tunnel-server.rb` only if you want a private instance instead of the hosted tunnel-rb.dev service. See [Quick start (self-hosted)](#quick-start-self-hosted).
|
|
64
|
+
|
|
65
|
+
## Start here (3 common scenarios)
|
|
66
|
+
|
|
67
|
+
1. Hosted default (fastest): `tunnel 3000`
|
|
68
|
+
2. Self-hosted local dev (plaintext server):
|
|
69
|
+
- Terminal 1: `ruby tunnel-server.rb`
|
|
70
|
+
- Terminal 2: `ruby tunnel.rb 3000 --server-host localhost --no-tls`
|
|
71
|
+
3. Self-hosted production (TLS + reverse proxy):
|
|
72
|
+
- Set `RELAY_DOMAIN`, `RELAY_TLS_CERT`, `RELAY_TLS_KEY`, and usually `RELAY_URL_PORT=`
|
|
73
|
+
- Start: `ruby tunnel-server.rb`
|
|
74
|
+
- Connect: `ruby tunnel.rb 3000 --server-host your-control-host`
|
|
75
|
+
|
|
76
|
+
### Library API
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
require "tunnel_rb"
|
|
80
|
+
|
|
81
|
+
TunnelRb::Client.new(
|
|
82
|
+
local_port: 3000,
|
|
83
|
+
server_host: "server.tunnel-rb.dev", # hosted server (same default as the CLI)
|
|
84
|
+
server_port: 7777,
|
|
85
|
+
tls: true
|
|
86
|
+
).start
|
|
87
|
+
|
|
88
|
+
TunnelRb::Server.new(
|
|
89
|
+
control_port: 7777,
|
|
90
|
+
public_port: 8080,
|
|
91
|
+
domain: "localhost"
|
|
92
|
+
).start
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Architecture
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
Browser Tunnel Server Tunnel Client Local App
|
|
99
|
+
| | | |
|
|
100
|
+
| HTTP (public port) | | |
|
|
101
|
+
|--------------------------->| new_connection (control port) | |
|
|
102
|
+
| |------------------------------------->| |
|
|
103
|
+
| | bind + data socket (control port) | |
|
|
104
|
+
| |<-------------------------------------| |
|
|
105
|
+
| | proxy bytes | TCP (local port) |
|
|
106
|
+
| |------------------------------------->|----------------------->|
|
|
107
|
+
| |<-------------------------------------|<-----------------------|
|
|
108
|
+
|<---------------------------| | |
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The tunnel server listens on two ports:
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
| Port | Role | Who connects |
|
|
115
|
+
| ------------------ | -------------------------------------------- | -------------- |
|
|
116
|
+
| **7777** (control) | Registration, ping/pong, data-socket handoff | Tunnel clients |
|
|
117
|
+
| **8080** (public) | Incoming HTTP from browsers / nginx | End users |
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
Each tunneled HTTP request uses **two** TCP connections on the control port:
|
|
121
|
+
|
|
122
|
+
1. The long-lived **control channel** (register, ping, `new_connection` commands).
|
|
123
|
+
2. A short-lived **data channel** (`bind` with a `conn_id`) that carries the actual HTTP bytes.
|
|
124
|
+
|
|
125
|
+
## Quick start (self-hosted)
|
|
126
|
+
|
|
127
|
+
Use this when you run your own tunnel server (e.g. local development or a private deployment) instead of the hosted tunnel-rb.dev service.
|
|
128
|
+
|
|
129
|
+
**Terminal 1 — tunnel server**
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
ruby tunnel-server.rb
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Terminal 2 — your local app** (example: Rails on 3000)
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
rails server -p 3000
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Terminal 3 — tunnel client**
|
|
142
|
+
|
|
143
|
+
The client connects over TLS by default. The local server above runs plaintext, so disable TLS with `--no-tls`:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
ruby tunnel.rb 3000 --no-tls
|
|
147
|
+
# after gem install: tunnel 3000 --no-tls
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The client prints a public URL, e.g.:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
🚀 [Tunnel] Ready! https://a1b2c3d4.localhost:8080 -> localhost:3000
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
For local plaintext testing, send a request over `http://` with the generated host:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
curl -H "Host: a1b2c3d4.localhost:8080" http://127.0.0.1:8080/
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The `Host` header must match the assigned subdomain. For local testing the default domain is `localhost` and the URL includes the public port (`:8080`), so curling `127.0.0.1:8080` with `Host: <subdomain>.localhost:8080` needs no `/etc/hosts` edits.
|
|
163
|
+
|
|
164
|
+
> Note: the registration URL is always printed as `https://...` because production traffic is expected behind a TLS edge (e.g. nginx). In local self-hosted mode (`ruby tunnel-server.rb` with no TLS on `public_port`), access the public port via `http://`.
|
|
165
|
+
|
|
166
|
+
Point the client at your server with `--server-host localhost --no-tls` (plaintext local server) or set `SERVER_HOST` / `SERVER_PORT` for a remote instance.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Tunnel server
|
|
171
|
+
|
|
172
|
+
### Running
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
ruby tunnel-server.rb
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Press **Ctrl+C** or send **SIGTERM** for graceful shutdown (listeners closed, thread pools drained, tokens persisted).
|
|
179
|
+
|
|
180
|
+
### Configuration
|
|
181
|
+
|
|
182
|
+
`TunnelRb::Server` accepts keyword arguments:
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
| Option | Default | Description |
|
|
186
|
+
| --------------- | ----------------------------------------- | ----------------------------------------------------------------------------- |
|
|
187
|
+
| `control_port` | _(required)_ | Tunnel client control plane |
|
|
188
|
+
| `public_port` | _(required)_ | Public HTTP edge (port the server binds/listens on) |
|
|
189
|
+
| `domain` | _(required)_ | Base domain for the registration URL (`https://{subdomain}.{domain}`) |
|
|
190
|
+
| `url_port` | `nil` | Port shown in the registration URL; `nil` omits it (e.g. nginx on 443) |
|
|
191
|
+
| `tokens_path` | `/tmp/tunnel-rb-server-tokens.json` | Persistent token store |
|
|
192
|
+
| `tls_cert` | `nil` | PEM cert path; enables TLS on the control port when set with `tls_key` |
|
|
193
|
+
| `tls_key` | `nil` | PEM private key path; enables TLS on the control port when set with `tls_cert`|
|
|
194
|
+
| `logger` | `TunnelRb::Server::Logger.new` | Injectable logger (stdout/stderr) |
|
|
195
|
+
|
|
196
|
+
`control_port`, `public_port`, and `domain` are required keyword arguments. When started via `ruby tunnel-server.rb` they're supplied from environment variables (defaults shown):
|
|
197
|
+
|
|
198
|
+
| Env var | Default | Maps to |
|
|
199
|
+
| -------------------- | ---------------- | -------------- |
|
|
200
|
+
| `RELAY_CONTROL_PORT` | `7777` | `control_port` |
|
|
201
|
+
| `RELAY_PUBLIC_PORT` | `8080` | `public_port` |
|
|
202
|
+
| `RELAY_DOMAIN` | `localhost` | `domain` |
|
|
203
|
+
| `RELAY_URL_PORT` | `public_port` | `url_port` |
|
|
204
|
+
|
|
205
|
+
`RELAY_URL_PORT` defaults to the public port, so locally the printed URL is `https://<subdomain>.localhost:8080`. Behind a reverse proxy (e.g. nginx terminating TLS on 443), the server's `8080` is internal — set `RELAY_URL_PORT=` (empty) so the URL drops the port:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# Local dev: URL shows the port (https://<sub>.localhost:8080)
|
|
209
|
+
ruby tunnel-server.rb
|
|
210
|
+
|
|
211
|
+
# Production behind nginx: clients see https://<sub>.tunnel.example.com (no port)
|
|
212
|
+
RELAY_DOMAIN=tunnel.example.com RELAY_URL_PORT= ruby tunnel-server.rb
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Example with custom options:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
require "tunnel_rb/server"
|
|
219
|
+
|
|
220
|
+
TunnelRb::Server.new(
|
|
221
|
+
control_port: 7777,
|
|
222
|
+
public_port: 8080,
|
|
223
|
+
domain: "tunnel.example.com",
|
|
224
|
+
tokens_path: "/var/lib/tunnel-rb/tokens.json"
|
|
225
|
+
).start
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Server components
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
lib/tunnel_rb/server/
|
|
232
|
+
server.rb Coordinator — wires components, signal handlers, shutdown
|
|
233
|
+
control_server.rb Port 7777 — accept, handshake pool, read loop, ping loop
|
|
234
|
+
public_server.rb Port 8080 — HTTP routing, pending connections, byte proxy
|
|
235
|
+
client_registry.rb Connected clients (subdomain ↔ socket)
|
|
236
|
+
token_store.rb Token persistence and subdomain assignment
|
|
237
|
+
pending_connections.rb conn_id ↔ Queue handoff between public and control sides
|
|
238
|
+
thread_pool.rb Bounded worker pools with backpressure
|
|
239
|
+
http_request.rb Header parsing + X-Forwarded-* injection
|
|
240
|
+
socket_helpers.rb TCP keepalive tuning
|
|
241
|
+
tls.rb Optional TLS context + listener wrapping for the control port
|
|
242
|
+
logger.rb Structured logging wrapper
|
|
243
|
+
client.rb Per-client state object
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Limits and behaviour
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
| Setting | Value | Effect |
|
|
250
|
+
| ------------------------ | ----------------------- | ------------------------------------------------ |
|
|
251
|
+
| Handshake pool | 64 workers + 64 queue | Backpressure on control-port connection floods |
|
|
252
|
+
| Public pool | 200 workers + 200 queue | Backpressure on HTTP connection floods |
|
|
253
|
+
| Ping interval | 30 s | Keeps NAT mappings alive |
|
|
254
|
+
| Missed pongs | 3 | Unresponsive clients are disconnected |
|
|
255
|
+
| HTTP header read timeout | 5 s | Slowloris protection on public port |
|
|
256
|
+
| Data socket wait | 10 s | Timeout if client does not bind in time |
|
|
257
|
+
| Control line max | 16 KiB | Slowloris protection on control read loop |
|
|
258
|
+
| Token TTL | 24 h | Tokens expire when no client holds the subdomain |
|
|
259
|
+
| Token cleanup | every 10 min | Background sweep of expired tokens |
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
### Token persistence
|
|
263
|
+
|
|
264
|
+
On first registration the server assigns a random subdomain (e.g. `a1b2c3d4`) and issues a reconnect **token**. Tokens are written to disk so a client can reconnect after a disconnect and keep the same subdomain.
|
|
265
|
+
|
|
266
|
+
- Connected clients: token stays valid regardless of TTL.
|
|
267
|
+
- Disconnected clients: token expires after 24 hours unless the client reconnects in time.
|
|
268
|
+
|
|
269
|
+
### Production notes
|
|
270
|
+
|
|
271
|
+
- Put **nginx** (or similar) in front of the public port for TLS termination. Forward `Host` unchanged so subdomain routing works.
|
|
272
|
+
- The server binds `0.0.0.0` on both ports.
|
|
273
|
+
- There is no authentication beyond the reconnect token — treat the control port as trusted infrastructure.
|
|
274
|
+
- Environment variables currently keep the `RELAY_*` prefix for backward compatibility.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## TLS (optional)
|
|
279
|
+
|
|
280
|
+
TLS can be enabled on the **control port** (`7777`) to encrypt the server ↔ tunnel-client link. Because both the long-lived control channel and the short-lived data sockets flow through this port, enabling it encrypts all traffic between the client and the server. The public HTTP port (`8080`) is unaffected — keep terminating its TLS at nginx as before.
|
|
281
|
+
|
|
282
|
+
The two sides default differently: the **server** runs plaintext unless a cert and key are supplied, while the **client** connects over TLS (and verifies against the system CA store) **by default** — use `--no-tls` to talk to a plaintext server.
|
|
283
|
+
|
|
284
|
+
### Generating a self-signed cert (for testing)
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365 -subj "/CN=localhost"
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Server
|
|
291
|
+
|
|
292
|
+
Pass `tls_cert` / `tls_key`, or set the `RELAY_TLS_CERT` / `RELAY_TLS_KEY` env vars when starting the server:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
RELAY_TLS_CERT=server.crt RELAY_TLS_KEY=server.key ruby tunnel-server.rb
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
TunnelRb::Server.new(
|
|
300
|
+
control_port: 7777,
|
|
301
|
+
public_port: 8080,
|
|
302
|
+
tls_cert: "server.crt",
|
|
303
|
+
tls_key: "server.key"
|
|
304
|
+
).start
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
On startup the server logs whether the control plane is running with TLS enabled.
|
|
308
|
+
|
|
309
|
+
For **Let's Encrypt**, point `tls_cert` / `RELAY_TLS_CERT` at **`fullchain.pem`**, not `cert.pem`. The server sends the entire chain to clients; a leaf cert alone causes `certificate verify failed (unable to get local issuer certificate)` on the client. A single PEM file (e.g. a self-signed test cert) still works — the server loads one or more certificates from the file.
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
RELAY_TLS_CERT=/etc/letsencrypt/live/server.example.com/fullchain.pem \
|
|
313
|
+
RELAY_TLS_KEY=/etc/letsencrypt/live/server.example.com/privkey.pem \
|
|
314
|
+
ruby tunnel-server.rb
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Client
|
|
318
|
+
|
|
319
|
+
TLS and certificate verification are **on by default**. Connecting to a server with a publicly trusted cert needs no flags:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
# Default: TLS on, verified against the system CA store (e.g. a Let's Encrypt cert).
|
|
323
|
+
ruby tunnel.rb 3000 --server-host server.example.com
|
|
324
|
+
|
|
325
|
+
# Custom CA: verify against a specific CA bundle instead of the system store.
|
|
326
|
+
ruby tunnel.rb 3000 --server-host server.example.com --tls-ca ca.pem
|
|
327
|
+
|
|
328
|
+
# Self-signed server: keep TLS but skip verification.
|
|
329
|
+
ruby tunnel.rb 3000 --server-host server.example.com --no-tls-verify
|
|
330
|
+
|
|
331
|
+
# Plaintext server (local dev / tests): disable TLS entirely.
|
|
332
|
+
ruby tunnel.rb 3000 --no-tls
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The same options are available via `RELAY_TLS` / `RELAY_TLS_VERIFY` (`1`/`true`/`yes`, default on) and `RELAY_TLS_CA`.
|
|
336
|
+
|
|
337
|
+
`--[no-]tls` applies **only to connections to the server** (control and data sockets). The link to your local app (e.g. Rails on `:3000`) is always plain TCP — even when server TLS is on.
|
|
338
|
+
|
|
339
|
+
The client has three TLS verification modes (TLS itself is toggled with `--[no-]tls`):
|
|
340
|
+
|
|
341
|
+
| Mode | Flags | Verification |
|
|
342
|
+
| ------------- | --------------------------- | ------------------------------------------------------------- |
|
|
343
|
+
| System CAs | _(default)_ | `VERIFY_PEER` against the OS trusted CA store + hostname check |
|
|
344
|
+
| Custom CA | `--tls-ca PATH` | `VERIFY_PEER` against the given CA cert/bundle + hostname check |
|
|
345
|
+
| Insecure | `--no-tls-verify` | None (`VERIFY_NONE`) — accepts any cert, for self-signed servers |
|
|
346
|
+
|
|
347
|
+
`--tls-ca` takes precedence over the system-CA default if both are in effect.
|
|
348
|
+
|
|
349
|
+
> Note: because verification is on by default, a self-signed server needs `--no-tls-verify`, and a plaintext server needs `--no-tls`. Both verifying modes include a hostname check.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Tunnel client
|
|
354
|
+
|
|
355
|
+
### Running
|
|
356
|
+
|
|
357
|
+
By default the client uses the hosted server — just pass the local port:
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
tunnel 3000
|
|
361
|
+
ruby tunnel.rb 3000 --help
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
For a self-hosted server:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
ruby tunnel.rb 3000 --server-host localhost --no-tls # local plaintext server
|
|
368
|
+
ruby tunnel.rb 3000 --server-host server.example.com # your own deployment
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
TLS is on by default (see [TLS](#tls-optional)); pass `--no-tls` when the server is plaintext (local dev with `ruby tunnel-server.rb`).
|
|
372
|
+
|
|
373
|
+
The local port can be passed as the first positional argument or via the `LOCAL_PORT` environment variable. The client exits with status 1 if neither is set.
|
|
374
|
+
|
|
375
|
+
### Configuration
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
| Flag | Env var | Default | Description |
|
|
379
|
+
| -------------- | -------------- | ----------- | ---------------------------------------------------------------- |
|
|
380
|
+
| (positional) | `LOCAL_PORT` | — | Port of the local service (required) |
|
|
381
|
+
| `--local-host` | `LOCAL_HOST` | `localhost` | Host of the local service |
|
|
382
|
+
| `--server-host` | `SERVER_HOST` | `server.tunnel-rb.dev` | Server control host; default is the hosted tunnel-rb.dev service |
|
|
383
|
+
| `--server-port` | `SERVER_PORT` | `7777` | Server control port; only change for self-hosted servers |
|
|
384
|
+
| `--[no-]tls` | `RELAY_TLS` | `true` | Connect to the server over TLS (use `--no-tls` for local/testing) |
|
|
385
|
+
| `--[no-]tls-verify` | `RELAY_TLS_VERIFY` | `true` | Verify the server cert against the system CA store |
|
|
386
|
+
| `--tls-ca` | `RELAY_TLS_CA` | — | CA cert/bundle to verify the server instead of the system store |
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
Examples:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
SERVER_HOST=server.example.com LOCAL_PORT=3000 ruby tunnel.rb
|
|
393
|
+
ruby tunnel.rb 3000 --local-host 127.0.0.1
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Behaviour
|
|
397
|
+
|
|
398
|
+
1. Opens a connection to the server control port (5 s connect timeout), wrapped in TLS unless `--no-tls` is set.
|
|
399
|
+
2. Sends `register` (with optional `token` on reconnect).
|
|
400
|
+
3. Receives `{ status: "ok", url: "...", token: "..." }`.
|
|
401
|
+
4. Enters a read loop on the control socket:
|
|
402
|
+
- `**ping**` → replies with `**pong**` (keeps the connection alive through NAT).
|
|
403
|
+
- `**new_connection**` → opens a fresh connection to the server (TLS when enabled, plain TCP with `--no-tls`), sends `bind` with the given `conn_id`, then proxies bytes between server and the local service over plain TCP.
|
|
404
|
+
5. If the local service is unreachable (connection refused, host unreachable, DNS failure, connect timeout), the client sends a `502 Bad Gateway` HTTP response back through the server instead of dropping the connection.
|
|
405
|
+
6. On disconnect, the client reconnects automatically with **exponential backoff** (1 s → 2 s → 4 s → … capped at 30 s, reset to 1 s after a successful registration), reusing the saved token.
|
|
406
|
+
|
|
407
|
+
### Reconnection
|
|
408
|
+
|
|
409
|
+
The client stores the token from the registration response. On reconnect it sends:
|
|
410
|
+
|
|
411
|
+
```json
|
|
412
|
+
{"action":"register","token":"<hex-token>"}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The server revokes the old token, closes the previous control socket, and restores the same subdomain.
|
|
416
|
+
|
|
417
|
+
### TCP keepalive
|
|
418
|
+
|
|
419
|
+
Both client and server enable TCP keepalive (idle 60 s, interval 30 s, 3 probes) to detect dead peers through NAT/firewalls.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Wire protocol
|
|
424
|
+
|
|
425
|
+
All messages are **newline-delimited JSON** (one object per line).
|
|
426
|
+
|
|
427
|
+
### Client → server (first message on every TCP connection)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
| `action` | Fields | Purpose |
|
|
431
|
+
| ---------- | ------------------ | ------------------------------------------------- |
|
|
432
|
+
| `register` | `token` (optional) | Claim or reclaim a subdomain |
|
|
433
|
+
| `bind` | `conn_id` | Attach a data socket to a pending browser request |
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
### Server → client (on control channel)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
| `action` / field | Purpose |
|
|
440
|
+
| ----------------------------------------------- | --------------------- |
|
|
441
|
+
| `{ status: "ok", url: "...", token: "..." }` | Registration response |
|
|
442
|
+
| `{ action: "ping" }` | Liveness check |
|
|
443
|
+
| `{ action: "new_connection", conn_id: "uuid" }` | Open a data channel |
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
### Client → server (on control channel, after register)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
| `action` | Purpose |
|
|
450
|
+
| -------------------- | ------------- |
|
|
451
|
+
| `{ action: "pong" }` | Reply to ping |
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
### Request flow (one HTTP request)
|
|
455
|
+
|
|
456
|
+
```
|
|
457
|
+
1. Browser → server:8080 GET /path Host: xxxx.tunnel-rb.dev
|
|
458
|
+
2. Server → client (control): {"action":"new_connection","conn_id":"<uuid>"}
|
|
459
|
+
3. Client → server (new TCP): {"action":"bind","conn_id":"<uuid>"}
|
|
460
|
+
4. Server forwards buffered HTTP headers on the data socket
|
|
461
|
+
5. Client connects to localhost:3000, proxies bytes both ways
|
|
462
|
+
6. Connection closes when either side finishes
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Testing
|
|
468
|
+
|
|
469
|
+
Requires Ruby stdlib only (minitest).
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
# All tests
|
|
473
|
+
bundle exec rake test
|
|
474
|
+
|
|
475
|
+
# or manually:
|
|
476
|
+
ruby -Ilib -Itest -e 'Dir["test/**/*_test.rb"].each { |f| require_relative f }'
|
|
477
|
+
|
|
478
|
+
# Unit tests (TokenStore)
|
|
479
|
+
ruby -Ilib -Itest test/server/token_store_test.rb
|
|
480
|
+
|
|
481
|
+
# End-to-end (real server on ephemeral ports, fake tunnel client, HTTP request)
|
|
482
|
+
ruby -Ilib -Itest test/server/integration_test.rb
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Integration tests start a real `TunnelRb::Server` on random free ports with an isolated token file. They do **not** touch `/tmp/tunnel-rb-server-tokens.json`.
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
## Project layout
|
|
490
|
+
|
|
491
|
+
```
|
|
492
|
+
exe/tunnel Gem executable (client)
|
|
493
|
+
lib/tunnel_rb/ Client library + CLI
|
|
494
|
+
lib/tunnel_rb/server/ Server (run from checkout: ruby tunnel-server.rb)
|
|
495
|
+
tunnel-rb.gemspec Gem specification
|
|
496
|
+
bin/ Development entry points
|
|
497
|
+
tunnel.rb Development shim (client)
|
|
498
|
+
tunnel-server.rb Server entry point (checkout only)
|
|
499
|
+
test/server/ Unit and integration tests
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## Requirements
|
|
503
|
+
|
|
504
|
+
- Ruby 3.x (tested on 3.4)
|
|
505
|
+
- No runtime gem dependencies (stdlib only)
|
|
506
|
+
|
data/exe/tunnel
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
require_relative "client"
|
|
6
|
+
|
|
7
|
+
module TunnelRb
|
|
8
|
+
module CLI
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def run(argv = ARGV)
|
|
12
|
+
Client.new(**parse_options(argv)).start
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def env_bool(name, default)
|
|
16
|
+
value = ENV[name]
|
|
17
|
+
return default if value.nil? || value.empty?
|
|
18
|
+
|
|
19
|
+
["1", "true", "yes"].include?(value.downcase)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse_options(argv)
|
|
23
|
+
options = {
|
|
24
|
+
server_host: ENV.fetch("SERVER_HOST", "server.tunnel-rb.dev"),
|
|
25
|
+
server_port: Integer(ENV.fetch("SERVER_PORT", "7777")),
|
|
26
|
+
local_host: ENV.fetch("LOCAL_HOST", "localhost"),
|
|
27
|
+
local_port: ENV["LOCAL_PORT"] ? Integer(ENV["LOCAL_PORT"]) : nil,
|
|
28
|
+
tls: env_bool("RELAY_TLS", true),
|
|
29
|
+
tls_ca: ENV["RELAY_TLS_CA"],
|
|
30
|
+
tls_verify: env_bool("RELAY_TLS_VERIFY", true)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
parser = OptionParser.new do |opts|
|
|
34
|
+
opts.banner = "Usage: tunnel [LOCAL_PORT] [options]"
|
|
35
|
+
opts.on("--server-host HOST", "Server host (default: #{options[:server_host]})") { |v| options[:server_host] = v }
|
|
36
|
+
opts.on("--server-port PORT", Integer, "Server port (default: #{options[:server_port]})") { |v| options[:server_port] = v }
|
|
37
|
+
opts.on("--local-host HOST", "Local host (default: #{options[:local_host]})") { |v| options[:local_host] = v }
|
|
38
|
+
opts.on("--[no-]tls", "Connect to the server over TLS (default: on; use --no-tls for local/testing)") { |v| options[:tls] = v }
|
|
39
|
+
opts.on("--[no-]tls-verify", "Verify the server cert against the system CA store (default: on)") { |v| options[:tls_verify] = v }
|
|
40
|
+
opts.on("--tls-ca PATH", "CA cert/bundle to verify the server instead of the system CA store") { |v| options[:tls_ca] = v }
|
|
41
|
+
opts.on("-h", "--help", "Show this help") do
|
|
42
|
+
puts opts
|
|
43
|
+
exit
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
parser.parse!(argv)
|
|
47
|
+
|
|
48
|
+
options[:local_port] = Integer(argv[0]) if argv[0]
|
|
49
|
+
|
|
50
|
+
if options[:local_port].nil?
|
|
51
|
+
warn "Error: local port is required (positional argument or LOCAL_PORT env var)"
|
|
52
|
+
warn parser.help
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
options
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|