portok 1.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/.dockerignore ADDED
@@ -0,0 +1,10 @@
1
+ node_modules
2
+ npm-debug.log
3
+ .git
4
+ .gitignore
5
+ *.md
6
+ !README.md
7
+ .DS_Store
8
+ coverage
9
+ *.state.json
10
+
package/Dockerfile ADDED
@@ -0,0 +1,41 @@
1
+ # Portok Test & Benchmark Environment
2
+ # Multi-stage build for development and testing on Linux
3
+
4
+ FROM node:20-alpine AS base
5
+
6
+ WORKDIR /app
7
+
8
+ # Install dependencies
9
+ COPY package*.json ./
10
+ RUN npm ci
11
+
12
+ # Copy source files
13
+ COPY . .
14
+
15
+ # Make CLI executable
16
+ RUN chmod +x portok.mjs portokd.mjs
17
+
18
+ # Default command runs tests
19
+ CMD ["npm", "test"]
20
+
21
+ # =============================================================================
22
+ # Test stage
23
+ # =============================================================================
24
+ FROM base AS test
25
+ ENV NODE_ENV=test
26
+ CMD ["npm", "test"]
27
+
28
+ # =============================================================================
29
+ # Benchmark stage
30
+ # =============================================================================
31
+ FROM base AS bench
32
+ ENV NODE_ENV=production
33
+ CMD ["npm", "run", "bench"]
34
+
35
+ # =============================================================================
36
+ # Development stage (with shell access)
37
+ # =============================================================================
38
+ FROM base AS dev
39
+ RUN apk add --no-cache bash curl
40
+ CMD ["/bin/bash"]
41
+
package/README.md ADDED
@@ -0,0 +1,606 @@
1
+ # Portok
2
+
3
+ A lightweight Node.js "switchboard" proxy that enables zero-downtime deployments by routing a stable public port to an internal app instance running on a random port, switching only when the new instance is healthy.
4
+
5
+ ## Features
6
+
7
+ - **Zero-downtime switching**: Health-gated port switching with connection draining
8
+ - **Auto-rollback**: Automatic rollback if the new port becomes unhealthy
9
+ - **WebSocket support**: Full HTTP and WebSocket proxying
10
+ - **Lightweight metrics**: Built-in metrics without heavy dependencies
11
+ - **Security**: Token-based auth, IP allowlist, rate limiting
12
+ - **CLI**: Easy-to-use command-line interface
13
+
14
+ ## Quick Start
15
+
16
+ ### Installation
17
+
18
+ ```bash
19
+ npm install
20
+ ```
21
+
22
+ ### Start the Daemon
23
+
24
+ ```bash
25
+ # Required environment variables
26
+ export LISTEN_PORT=3000
27
+ export INITIAL_TARGET_PORT=8080
28
+ export ADMIN_TOKEN=your-secret-token
29
+
30
+ # Start portokd
31
+ node portokd.mjs
32
+ ```
33
+
34
+ ### Use the CLI
35
+
36
+ ```bash
37
+ # Check status
38
+ ./portok.mjs status --token your-secret-token
39
+
40
+ # Switch to new port
41
+ ./portok.mjs switch 8081 --token your-secret-token
42
+
43
+ # Check metrics
44
+ ./portok.mjs metrics --token your-secret-token
45
+
46
+ # Check health
47
+ ./portok.mjs health --token your-secret-token
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ All configuration is via environment variables:
53
+
54
+ | Variable | Default | Description |
55
+ |----------|---------|-------------|
56
+ | `INSTANCE_NAME` | `default` | Instance identifier (for logging/state file naming) |
57
+ | `LISTEN_PORT` | `3000` | Port the proxy listens on |
58
+ | `INITIAL_TARGET_PORT` | (required) | Initial backend port to proxy to |
59
+ | `STATE_FILE` | `/var/lib/portok/<instance>.json` | Path to persist state |
60
+ | `HEALTH_PATH` | `/health` | Health check endpoint path |
61
+ | `HEALTH_TIMEOUT_MS` | `5000` | Health check timeout |
62
+ | `DRAIN_MS` | `30000` | Connection drain period after switch |
63
+ | `ROLLBACK_WINDOW_MS` | `60000` | Time window for auto-rollback monitoring |
64
+ | `ROLLBACK_CHECK_EVERY_MS` | `5000` | Health check interval during rollback window |
65
+ | `ROLLBACK_FAIL_THRESHOLD` | `3` | Consecutive failures before rollback |
66
+ | `ADMIN_TOKEN` | (required) | Token for admin endpoint authentication |
67
+ | `ADMIN_ALLOWLIST` | `127.0.0.1,::1` | Comma-separated list of allowed IPs |
68
+ | `ADMIN_UNIX_SOCKET` | (optional) | Unix socket path for admin endpoints |
69
+
70
+ ### Performance Tuning
71
+
72
+ | Variable | Default | Description |
73
+ |----------|---------|-------------|
74
+ | `FAST_PATH` | `0` | Enable minimal metrics mode for maximum throughput |
75
+ | `UPSTREAM_KEEPALIVE` | `1` | Enable keep-alive for upstream connections (critical) |
76
+ | `UPSTREAM_MAX_SOCKETS` | `1024` | Maximum sockets per upstream host |
77
+ | `UPSTREAM_KEEPALIVE_MSECS` | `1000` | Keep-alive ping interval in ms |
78
+ | `SERVER_KEEPALIVE_TIMEOUT` | `5000` | Server keep-alive timeout in ms |
79
+ | `SERVER_HEADERS_TIMEOUT` | `6000` | Headers timeout (must be > keepAliveTimeout) |
80
+ | `ENABLE_XFWD` | `1` | Add X-Forwarded-* headers to proxied requests |
81
+ | `DEBUG_UPSTREAM` | `0` | Track upstream socket creation in /__metrics |
82
+ | `VERBOSE_ERRORS` | `0` | Log full error stacks (disable in production) |
83
+
84
+ ## Admin Endpoints
85
+
86
+ All admin endpoints require the `x-admin-token` header.
87
+
88
+ ### GET /__status
89
+
90
+ Returns current proxy status.
91
+
92
+ ```bash
93
+ curl -H "x-admin-token: your-token" http://127.0.0.1:3000/__status
94
+ ```
95
+
96
+ Response:
97
+ ```json
98
+ {
99
+ "activePort": 8080,
100
+ "drainUntil": null,
101
+ "lastSwitch": {
102
+ "from": 8081,
103
+ "to": 8080,
104
+ "at": "2024-01-15T10:30:00.000Z",
105
+ "reason": "manual",
106
+ "id": "uuid-here"
107
+ }
108
+ }
109
+ ```
110
+
111
+ ### GET /__metrics
112
+
113
+ Returns proxy metrics.
114
+
115
+ ```bash
116
+ curl -H "x-admin-token: your-token" http://127.0.0.1:3000/__metrics
117
+ ```
118
+
119
+ Response:
120
+ ```json
121
+ {
122
+ "startedAt": "2024-01-15T10:00:00.000Z",
123
+ "inflight": 5,
124
+ "inflightMax": 100,
125
+ "totalRequests": 50000,
126
+ "totalProxyErrors": 2,
127
+ "statusCounters": {
128
+ "2xx": 49500,
129
+ "3xx": 100,
130
+ "4xx": 398,
131
+ "5xx": 2
132
+ },
133
+ "rollingRps60": 125.5,
134
+ "health": {
135
+ "activePortOk": true,
136
+ "lastCheckedAt": "2024-01-15T10:29:55.000Z",
137
+ "consecutiveFails": 0
138
+ },
139
+ "lastProxyError": null
140
+ }
141
+ ```
142
+
143
+ ### POST /__switch?port=PORT
144
+
145
+ Switch to a new target port. Performs health check before switching.
146
+
147
+ ```bash
148
+ curl -X POST -H "x-admin-token: your-token" \
149
+ "http://127.0.0.1:3000/__switch?port=8081"
150
+ ```
151
+
152
+ Success response (200):
153
+ ```json
154
+ {
155
+ "success": true,
156
+ "message": "Switched to port 8081",
157
+ "switch": {
158
+ "from": 8080,
159
+ "to": 8081,
160
+ "at": "2024-01-15T10:30:00.000Z",
161
+ "reason": "manual",
162
+ "id": "uuid-here"
163
+ }
164
+ }
165
+ ```
166
+
167
+ Failure response (409 - health check failed):
168
+ ```json
169
+ {
170
+ "error": "Health check failed",
171
+ "message": "Port 8081 did not respond with 2xx at /health"
172
+ }
173
+ ```
174
+
175
+ ### GET /__health
176
+
177
+ Check health of the current active port.
178
+
179
+ ```bash
180
+ curl -H "x-admin-token: your-token" http://127.0.0.1:3000/__health
181
+ ```
182
+
183
+ ## CLI Reference
184
+
185
+ ```
186
+ portok <command> [options]
187
+
188
+ Management Commands:
189
+ init Initialize portok directories (/etc/portok, /var/lib/portok)
190
+ add <name> Create a new service instance
191
+ list List all configured instances and their status
192
+
193
+ Operational Commands:
194
+ status Show current proxy status
195
+ metrics Show proxy metrics
196
+ switch <port> Switch to a new target port
197
+ health Check health of active port
198
+
199
+ Options:
200
+ --url <url> Daemon URL (default: http://127.0.0.1:3000)
201
+ --instance <name> Target instance by name (reads /etc/portok/<name>.env)
202
+ --token <token> Admin token (or PORTOK_TOKEN env var)
203
+ --json Output as JSON
204
+ --help Show help
205
+
206
+ Options for 'add' command:
207
+ --port <port> Listen port (default: random 3000-3999)
208
+ --target <port> Target port (default: random 8000-8999)
209
+ --health <path> Health check path (default: /health)
210
+ --force Overwrite existing config
211
+ ```
212
+
213
+ ### Quick Start with CLI
214
+
215
+ ```bash
216
+ # 1. Initialize portok (creates /etc/portok and /var/lib/portok)
217
+ sudo portok init
218
+
219
+ # 2. Create a new service
220
+ sudo portok add api --port 3001 --target 8001
221
+
222
+ # 3. Start the service
223
+ sudo systemctl start portok@api
224
+
225
+ # 4. Check status
226
+ portok status --instance api
227
+
228
+ # 5. List all services
229
+ portok list
230
+ ```
231
+
232
+ ### Environment Variables for CLI
233
+
234
+ - `PORTOK_URL`: Default daemon URL
235
+ - `PORTOK_TOKEN`: Admin token
236
+
237
+ ### Examples
238
+
239
+ ```bash
240
+ # Initialize portok (run once)
241
+ sudo portok init
242
+
243
+ # Create services with specific ports
244
+ sudo portok add api --port 3001 --target 8001
245
+ sudo portok add web --port 3002 --target 8002
246
+
247
+ # List all instances with status
248
+ portok list
249
+
250
+ # Check status by instance name
251
+ portok status --instance api
252
+
253
+ # Get metrics as JSON
254
+ portok metrics --instance api --json
255
+
256
+ # Switch to new port
257
+ portok switch 8081 --instance api
258
+
259
+ # Health check (exits 0 if healthy, 1 if unhealthy)
260
+ portok health --instance api && echo "OK" || echo "FAIL"
261
+
262
+ # Direct URL mode (without instance)
263
+ portok status --url http://127.0.0.1:3000 --token your-token
264
+ ```
265
+
266
+ ## systemd Service
267
+
268
+ Example systemd unit file (`/etc/systemd/system/portokd.service`):
269
+
270
+ ```ini
271
+ [Unit]
272
+ Description=Portok Zero-Downtime Proxy
273
+ After=network.target
274
+
275
+ [Service]
276
+ Type=simple
277
+ User=www-data
278
+ WorkingDirectory=/opt/portok
279
+ ExecStart=/usr/bin/node /opt/portok/portokd.mjs
280
+ Restart=always
281
+ RestartSec=5
282
+
283
+ # Environment
284
+ Environment=LISTEN_PORT=3000
285
+ Environment=INITIAL_TARGET_PORT=8080
286
+ Environment=STATE_FILE=/var/lib/portok/state.json
287
+ Environment=ADMIN_TOKEN=your-secret-token
288
+ Environment=HEALTH_PATH=/health
289
+ Environment=DRAIN_MS=30000
290
+ Environment=ROLLBACK_WINDOW_MS=60000
291
+ Environment=ROLLBACK_CHECK_EVERY_MS=5000
292
+ Environment=ROLLBACK_FAIL_THRESHOLD=3
293
+
294
+ # Security hardening
295
+ NoNewPrivileges=true
296
+ ProtectSystem=strict
297
+ ProtectHome=true
298
+ ReadWritePaths=/var/lib/portok
299
+
300
+ [Install]
301
+ WantedBy=multi-user.target
302
+ ```
303
+
304
+ Enable and start:
305
+
306
+ ```bash
307
+ sudo systemctl daemon-reload
308
+ sudo systemctl enable portokd
309
+ sudo systemctl start portokd
310
+ ```
311
+
312
+ ## Testing
313
+
314
+ Tests run in Docker for Linux compatibility:
315
+
316
+ ```bash
317
+ # Run all tests
318
+ docker compose run --rm test
319
+
320
+ # Run specific test file
321
+ docker compose run --rm test npm test -- --test-name-pattern="proxy"
322
+
323
+ # Development shell
324
+ docker compose run --rm dev
325
+ ```
326
+
327
+ Or run locally (requires Node.js 20+):
328
+
329
+ ```bash
330
+ npm test
331
+ ```
332
+
333
+ ## Benchmarks
334
+
335
+ Benchmarks measure proxy performance:
336
+
337
+ ```bash
338
+ # Run all benchmarks in Docker
339
+ docker compose run --rm bench
340
+
341
+ # Quick benchmark (shorter duration)
342
+ docker compose run --rm bench npm run bench -- --quick
343
+
344
+ # Output JSON for CI
345
+ docker compose run --rm bench npm run bench -- --json > results.json
346
+ ```
347
+
348
+ Benchmark scenarios:
349
+
350
+ | Benchmark | Description |
351
+ |-----------|-------------|
352
+ | **Throughput** | Maximum requests/sec with 100 connections |
353
+ | **Latency** | Latency percentiles (p50, p95, p99) |
354
+ | **Connections** | Scaling with 10-500 concurrent connections |
355
+ | **Switching** | Switch latency and request loss |
356
+ | **Baseline** | Direct vs proxied overhead comparison |
357
+ | **Keep-Alive** | Validates keep-alive performance (RPS >= 70% of direct) |
358
+
359
+ ## Multi-Instance Setup
360
+
361
+ Portok supports running multiple isolated instances on the same host, each managing a different application. This is the recommended approach for multi-app deployments.
362
+
363
+ ### Directory Structure
364
+
365
+ ```
366
+ /etc/portok/
367
+ ├── api.env # Config for "api" instance
368
+ ├── web.env # Config for "web" instance
369
+ └── worker.env # Config for "worker" instance
370
+
371
+ /var/lib/portok/
372
+ ├── api.json # State file for "api" instance
373
+ ├── web.json # State file for "web" instance
374
+ └── worker.json # State file for "worker" instance
375
+ ```
376
+
377
+ ### Instance Configuration
378
+
379
+ Each instance has its own env file at `/etc/portok/<instance>.env`:
380
+
381
+ **Example: `/etc/portok/api.env`**
382
+ ```bash
383
+ # Required
384
+ LISTEN_PORT=3001
385
+ INITIAL_TARGET_PORT=8001
386
+ ADMIN_TOKEN=api-secret-token-change-me
387
+
388
+ # Optional (defaults shown)
389
+ HEALTH_PATH=/health
390
+ HEALTH_TIMEOUT_MS=5000
391
+ DRAIN_MS=30000
392
+ ROLLBACK_WINDOW_MS=60000
393
+ ROLLBACK_CHECK_EVERY_MS=5000
394
+ ROLLBACK_FAIL_THRESHOLD=3
395
+ ```
396
+
397
+ **Example: `/etc/portok/web.env`**
398
+ ```bash
399
+ LISTEN_PORT=3002
400
+ INITIAL_TARGET_PORT=8002
401
+ ADMIN_TOKEN=web-secret-token-change-me
402
+ ```
403
+
404
+ ### systemd Template Unit
405
+
406
+ Use the template unit `portok@.service` for managing instances:
407
+
408
+ ```bash
409
+ # Copy the template
410
+ sudo cp portok@.service /etc/systemd/system/
411
+
412
+ # Create config directory and state directory
413
+ sudo mkdir -p /etc/portok /var/lib/portok
414
+ sudo chown www-data:www-data /var/lib/portok
415
+
416
+ # Create instance configs
417
+ sudo cp examples/api.env /etc/portok/api.env
418
+ sudo cp examples/web.env /etc/portok/web.env
419
+
420
+ # Edit tokens and ports
421
+ sudo nano /etc/portok/api.env
422
+ sudo nano /etc/portok/web.env
423
+
424
+ # Reload systemd
425
+ sudo systemctl daemon-reload
426
+
427
+ # Start instances
428
+ sudo systemctl start portok@api
429
+ sudo systemctl start portok@web
430
+
431
+ # Enable at boot
432
+ sudo systemctl enable portok@api
433
+ sudo systemctl enable portok@web
434
+
435
+ # Check status
436
+ sudo systemctl status portok@api
437
+ sudo systemctl status portok@web
438
+
439
+ # View logs
440
+ journalctl -u portok@api -f
441
+ journalctl -u portok@web -f
442
+ ```
443
+
444
+ ### CLI with Multi-Instance
445
+
446
+ Use `--instance` to target a specific instance:
447
+
448
+ ```bash
449
+ # Target by instance name (reads /etc/portok/<name>.env)
450
+ portok status --instance api
451
+ portok metrics --instance web
452
+ portok switch 8081 --instance api
453
+ portok health --instance web --json
454
+
455
+ # Explicit URL/token still works (and overrides env file)
456
+ portok status --url http://127.0.0.1:3001 --token api-secret-token
457
+ ```
458
+
459
+ ### Instance Isolation
460
+
461
+ Each instance is fully isolated:
462
+ - **Separate state files**: No shared state between instances
463
+ - **Separate tokens**: Each instance has its own admin token
464
+ - **Separate metrics**: Metrics are per-instance
465
+ - **Separate rollback monitors**: Each instance tracks its own rollback window
466
+ - **Separate rate limits**: Rate limiting is per-instance
467
+
468
+ ### Multi-Instance Architecture
469
+
470
+ ```
471
+ ┌─────────────────────────────────────────────────────────────┐
472
+ │ Load Balancer / Nginx │
473
+ └───────────┬─────────────────────────┬───────────────────────┘
474
+ │ │
475
+ ▼ ▼
476
+ ┌───────────────────────┐ ┌───────────────────────┐
477
+ │ portok@api (:3001) │ │ portok@web (:3002) │
478
+ │ State: api.json │ │ State: web.json │
479
+ └───────────┬───────────┘ └───────────┬───────────┘
480
+ │ │
481
+ ▼ ▼
482
+ ┌───────────────────────┐ ┌───────────────────────┐
483
+ │ api-v1 (:8001) │ │ web-v1 (:8002) │
484
+ │ api-v2 (:8011) │ │ web-v2 (:8012) │
485
+ └───────────────────────┘ └───────────────────────┘
486
+ ```
487
+
488
+ ## Architecture
489
+
490
+ ```
491
+ ┌─────────────────────────────────────────────────────────┐
492
+ │ Client Traffic │
493
+ └─────────────────────────┬───────────────────────────────┘
494
+
495
+
496
+ ┌─────────────────────────────────────────────────────────┐
497
+ │ portokd (LISTEN_PORT) │
498
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
499
+ │ │ Proxy │ │ Admin │ │ Health Monitor │ │
500
+ │ │ (http-proxy)│ │ Endpoints │ │ (auto-rollback)│ │
501
+ │ └──────┬──────┘ └─────────────┘ └─────────────────┘ │
502
+ │ │ │
503
+ │ ┌──────┴──────┐ │
504
+ │ │Socket Tracker│ ← Maps connections to ports for drain │
505
+ │ └──────┬──────┘ │
506
+ └─────────┼───────────────────────────────────────────────┘
507
+
508
+
509
+ ┌─────────────────────────────────────────────────────────┐
510
+ │ 127.0.0.1:ACTIVE_PORT │
511
+ │ (Your App) │
512
+ └─────────────────────────────────────────────────────────┘
513
+ ```
514
+
515
+ ## Zero-Downtime Deployment Flow
516
+
517
+ 1. Deploy new app version on a random port (e.g., 54321)
518
+ 2. New app starts and exposes `/health` endpoint
519
+ 3. Call `portok switch 54321` or `POST /__switch?port=54321`
520
+ 4. Portok health-checks the new port
521
+ 5. If healthy: switch traffic, drain old connections
522
+ 6. If new port fails during rollback window: auto-rollback
523
+ 7. Old app can be stopped after drain period
524
+
525
+ ## Performance Notes
526
+
527
+ Portok is optimized for high throughput and low latency proxy operations.
528
+
529
+ ### FAST_PATH Mode (Recommended for High-Traffic)
530
+
531
+ Enable `FAST_PATH=1` for maximum throughput in production or benchmarks:
532
+
533
+ ```bash
534
+ export FAST_PATH=1
535
+ ```
536
+
537
+ This disables expensive metrics (status counters, rolling RPS) while keeping essential counters (totalRequests, inflight, proxyErrors).
538
+
539
+ ### Keep-Alive (Critical)
540
+
541
+ The upstream keep-alive agent is **critical for performance**. Without it, every request opens a new TCP connection which adds ~0.5-2ms latency and significantly limits throughput.
542
+
543
+ Keep-alive is enabled by default (`UPSTREAM_KEEPALIVE=1`). Do not disable it in production.
544
+
545
+ ### Performance Validation
546
+
547
+ Run the validation benchmark to verify performance:
548
+
549
+ ```bash
550
+ # Quick validation (3s)
551
+ FAST_PATH=1 node bench/validate.mjs --quick
552
+
553
+ # Full validation (10s)
554
+ FAST_PATH=1 node bench/validate.mjs
555
+
556
+ # Manual autocannon test
557
+ # Direct:
558
+ npx autocannon -c 50 -d 10 http://127.0.0.1:<APP_PORT>/
559
+
560
+ # Proxied:
561
+ npx autocannon -c 50 -d 10 http://127.0.0.1:<PROXY_PORT>/
562
+ ```
563
+
564
+ **Acceptance Criteria:**
565
+ - RPS >= 30% of direct (http-proxy adds inherent overhead)
566
+ - Added p50 latency <= 10ms
567
+ - p99 latency <= 50ms
568
+
569
+ **Typical Results (localhost, FAST_PATH=1):**
570
+ - Direct: ~28,000 RPS, p50=1ms
571
+ - Proxied: ~13,000 RPS, p50=3ms
572
+ - Socket reuse: 800-2000x (confirms keep-alive working)
573
+
574
+ ### Debug Upstream Connections
575
+
576
+ Enable `DEBUG_UPSTREAM=1` to track upstream socket creation in `/__metrics`:
577
+
578
+ ```bash
579
+ export DEBUG_UPSTREAM=1
580
+ ```
581
+
582
+ This exposes `upstreamSocketsCreated` in metrics to verify keep-alive is working.
583
+
584
+ ### Optimization Summary
585
+
586
+ | Optimization | Impact |
587
+ |--------------|--------|
588
+ | FAST_PATH mode | Minimal per-request overhead |
589
+ | Keep-alive agent | 10-20x throughput vs no keep-alive |
590
+ | Connection header stripping | Ensures upstream keep-alive works |
591
+ | Minimal URL parsing | No allocations in hot path |
592
+ | res.once() listeners | Auto-cleanup, no memory leaks |
593
+ | Socket reuse tracking | DEBUG_UPSTREAM confirms keep-alive |
594
+
595
+ ## Security
596
+
597
+ - **Token authentication**: All admin endpoints require `x-admin-token` header
598
+ - **Timing-safe comparison**: Token validation uses `crypto.timingSafeEqual`
599
+ - **IP allowlist**: Admin endpoints restricted by IP (default: localhost only)
600
+ - **Rate limiting**: 10 requests/minute per IP for admin endpoints
601
+ - **SSRF protection**: Target host fixed to `127.0.0.1`
602
+
603
+ ## License
604
+
605
+ MIT
606
+