vssh 3.7.2__tar.gz → 3.7.3__tar.gz

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.
vssh-3.7.3/PKG-INFO ADDED
@@ -0,0 +1,1030 @@
1
+ Metadata-Version: 2.4
2
+ Name: vssh
3
+ Version: 3.7.3
4
+ Summary: Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server
5
+ Author-email: MeshPOP <mpop@mpop.dev>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/meshpop/vssh
8
+ Project-URL: Repository, https://github.com/meshpop/vssh
9
+ Keywords: ssh,scp,tailscale,p2p,vpn,mcp,remote
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: System :: Networking
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # vssh
25
+
26
+ **SSH alternative and mesh transport layer — zero dependencies, faster connections, built for fleets.**
27
+
28
+ ```bash
29
+ pip install vssh
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Why vssh?
35
+
36
+ Managing multiple servers with standard SSH gets painful fast.
37
+
38
+ ```bash
39
+ # SSH — the old way
40
+ ssh -i ~/.ssh/id_ed25519 root@192.168.1.10 "systemctl restart nginx"
41
+ ssh -i ~/.ssh/id_ed25519 root@192.168.1.11 "systemctl restart nginx"
42
+ ssh -i ~/.ssh/id_ed25519 root@192.168.1.12 "systemctl restart nginx"
43
+ # ...repeat for every server
44
+ ```
45
+
46
+ ```bash
47
+ # vssh — the new way
48
+ vssh exec web1 "systemctl restart nginx"
49
+ vssh exec web2 "systemctl restart nginx"
50
+ vssh exec web3 "systemctl restart nginx"
51
+
52
+ # Or run across all nodes at once (via MCP / scripting)
53
+ for node in web1 web2 web3; do vssh exec $node "systemctl restart nginx" & done
54
+ ```
55
+
56
+ No key flags. No IP addresses. No repeated handshakes. Just node names.
57
+
58
+ ---
59
+
60
+ ## What is vssh?
61
+
62
+ vssh is a **standalone SSH alternative and mesh transport layer** that:
63
+
64
+ - Works **out of the box** after `pip install vssh` — no config files, no agents, no dependencies
65
+ - Auto-discovers servers from your [wire](https://github.com/meshpop/wire) VPN mesh or a static config file
66
+ - Authenticates via **HMAC-SHA256** shared secret instead of per-node key management
67
+ - Falls back to **Tailscale** automatically when the primary path is unreachable
68
+ - Runs a persistent **daemon** on each node, so connections are instant — no SSH handshake overhead
69
+
70
+ If you already use WireGuard mesh (via `wire`), vssh is the transport layer that sits on top and gives you fast, authenticated, name-based access to every node in the mesh.
71
+
72
+ If you don't use wire, vssh works standalone with a simple `~/.vssh/config` file listing your servers.
73
+
74
+ ---
75
+
76
+ ## Install
77
+
78
+ ```bash
79
+ pip install vssh
80
+ ```
81
+
82
+ Pure Python stdlib — no external packages required. Works on Linux and macOS.
83
+
84
+ Start the daemon on each server:
85
+
86
+ ```bash
87
+ # Set shared secret (same on all nodes)
88
+ export VSSH_SECRET=your-shared-secret
89
+
90
+ # Start daemon
91
+ vssh server
92
+ ```
93
+
94
+ Or as a systemd service:
95
+
96
+ ```bash
97
+ vssh install # writes and enables /etc/systemd/system/vssh.service
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Authentication
103
+
104
+ vssh uses **HMAC-SHA256** for authentication. Every connection includes a time-based token:
105
+
106
+ ```
107
+ hmac_{timestamp}_{sha256(secret, timestamp)[:32]}
108
+ ```
109
+
110
+ - The same `VSSH_SECRET` is shared across all nodes
111
+ - Tokens are valid for ±60 seconds (configurable clock skew window)
112
+ - No per-node keys, no certificates, no key rotation
113
+ - Secret is read from (in priority order):
114
+ 1. `VSSH_SECRET` environment variable
115
+ 2. `~/.vssh/secret` file
116
+ 3. `SECRET=` entry in `~/.vssh/config`
117
+ 4. Auto-generated on first install (`secrets.token_hex(16)`)
118
+
119
+ ---
120
+
121
+ ## Connection Protocol
122
+
123
+ All vssh communication happens on **TCP port 48291** using a simple line-delimited text protocol. Every command follows the format:
124
+
125
+ ```
126
+ COMMAND:auth_token:...args...\n
127
+ ```
128
+
129
+ The server responds with `OK` (or `OK:size`) followed by the payload and `__END__` as a terminator.
130
+
131
+ | Protocol | Format | Direction |
132
+ |---|---|---|
133
+ | SSH exec | `SSH:token:command\n` | client → server |
134
+ | File upload | `PUT:token:path:size:md5\n` + data | client → server |
135
+ | File download | `GET:token:path\n` | client → server |
136
+ | RPC call | `RPC:token:method:payload_len\n` + JSON | client → server |
137
+ | Node info | `INFO:token\n` | client → server |
138
+ | PTY session | `PTY_SESSION:token:rows:cols\n` | client → server |
139
+ | Persistent session | `SESSION:token\n` | client → server |
140
+
141
+ ---
142
+
143
+ ## CLI Commands
144
+
145
+ ### Status and Monitoring
146
+
147
+ ```bash
148
+ vssh status # Connection status to all mesh nodes
149
+ vssh status --full # Status + full node info (disk, memory, load)
150
+ vssh stats # Transfer statistics (last 7 days)
151
+ vssh stats 30 # Transfer statistics for last 30 days
152
+ vssh history # Last 20 commands
153
+ vssh history 50 # Last 50 commands
154
+ vssh history put # Filter by operation (PUT, GET, SSH, RPC, SYNC)
155
+ vssh history 20 web1 # Last 20 commands for specific host
156
+ ```
157
+
158
+ **Example `vssh status` output:**
159
+ ```
160
+ vssh v3.7.2 - Connection Status
161
+ ======================================================================
162
+ c1 10.99.248.51 ● online (12ms)
163
+ c2 10.99.14.187 ● online (11ms)
164
+ v5 10.99.171.140 ● online (local)
165
+
166
+ Total: 3/3 online
167
+ ```
168
+
169
+ **Example `vssh status --full` output:**
170
+ ```
171
+ vssh v3.7.2 - Cluster Status (full)
172
+ ======================================================================
173
+ web1 10.99.1.10 ● online (8ms)
174
+ disk: 45% used (120GB / 250GB) mem: 4.2GB / 16GB load: 0.42
175
+ web2 10.99.1.11 ● online (9ms)
176
+ disk: 31% used mem: 2.1GB / 8GB load: 0.15
177
+ db1 10.99.1.20 ✗ offline
178
+
179
+ Total: 2/3 online
180
+ ```
181
+
182
+ ---
183
+
184
+ ### Remote Execution
185
+
186
+ ```bash
187
+ vssh <host> # Interactive terminal (PTY — like SSH)
188
+ vssh <host> "command" # Single command
189
+ vssh ssh <host> "command" # Explicit ssh subcommand
190
+ vssh session <host> # Persistent terminal session (PTY mode)
191
+ vssh session-test <host> # Test session latency (3 RPC calls)
192
+ ```
193
+
194
+ **Examples:**
195
+ ```bash
196
+ vssh web1 # Open interactive shell
197
+ vssh web1 "uptime" # Run single command
198
+ vssh web1 "df -h && free -h" # Chain commands
199
+ vssh session web1 # Full PTY session (like SSH)
200
+ ```
201
+
202
+ ---
203
+
204
+ ### File Transfer
205
+
206
+ ```bash
207
+ vssh put <local> <host>:<remote> # Upload
208
+ vssh put -z <local> <host>:<remote> # Force compression
209
+ vssh put --resume <local> <host>:<remote> # Resume interrupted upload
210
+ vssh put --retry=3 <local> <host>:<remote> # Auto-retry on failure
211
+ vssh get <host>:<remote> <local> # Download
212
+ vssh get --retry=3 <host>:<remote> <local> # Download with retry
213
+ vssh sync <local_dir> <host>:<remote_dir> # Directory sync (8 parallel streams)
214
+ vssh mput <host>:<base_path> <file1> <file2> ... # Multi-file single connection
215
+ vssh fast <local> <host>:<remote> # Auto-select best method
216
+ vssh p2p <local> <host>:<remote> # P2P direct (NAT hole-punch)
217
+ vssh rsync <local> <host>:<remote> # Delta sync (only changed blocks)
218
+ ```
219
+
220
+ **Transfer flags:**
221
+ | Flag | Description |
222
+ |---|---|
223
+ | `-z` / `--compress` | Force zlib compression |
224
+ | `--no-compress` | Disable auto-compression |
225
+ | `--resume` | Resume interrupted transfer (for files >100MB) |
226
+ | `--retry=N` | Retry N times on network failure |
227
+ | `--limit=X` | Rate limit (e.g. `--limit=10MB`) |
228
+ | `--no-lan` | Disable LAN-direct optimization |
229
+
230
+ **Examples:**
231
+ ```bash
232
+ vssh put ./app.tar.gz web1:/opt/app.tar.gz # Upload
233
+ vssh get web1:/var/log/app.log ./app.log # Download
234
+ vssh sync ./config/ web1:/etc/app/ # Sync directory
235
+ vssh mput web1:/backup/ file1.txt file2.txt file3.log # Multi-file
236
+ vssh rsync ./src/ web1:/opt/src/ # Delta sync (only changes)
237
+ vssh p2p ./dataset.tar.gz web2:/data/ # P2P bypass VPN relay
238
+ ```
239
+
240
+ vssh auto-compresses text files (`.py`, `.js`, `.json`, `.log`, `.md`, etc.) and skips upload for files with matching MD5.
241
+
242
+ ---
243
+
244
+ ### Speed Test
245
+
246
+ ```bash
247
+ vssh speed-test <host> # Transfer speed test (10MB default)
248
+ vssh speed-test <host> --size=50 # Custom size in MB
249
+ ```
250
+
251
+ ```
252
+ → 54.2 MB/s (WireGuard mesh, direct)
253
+ ```
254
+
255
+ ---
256
+
257
+ ### Pipes (stdin/stdout)
258
+
259
+ ```bash
260
+ # Pipe stdin to remote file
261
+ cat local.log | vssh pipe-up web1:/var/log/backup.log
262
+
263
+ # Pipe remote command to stdout
264
+ vssh pipe-down web1:"journalctl -u nginx -n 100" | grep ERROR
265
+
266
+ # Auto-detect: stdin present → upload, otherwise → download
267
+ cat data.csv | vssh pipe web1:/data/input.csv
268
+ vssh pipe web1:"cat /etc/os-release" > os-info.txt
269
+ ```
270
+
271
+ ---
272
+
273
+ ### Node Info
274
+
275
+ ```bash
276
+ vssh info <host> # Full JSON node info (OS, IPs, vsshd version, etc.)
277
+ ```
278
+
279
+ ```json
280
+ {
281
+ "hostname": "web1",
282
+ "os": "Linux-5.15.0",
283
+ "vssh_version": "3.7.2",
284
+ "local_ips": ["10.99.1.10"],
285
+ "public_ip": "203.0.113.10",
286
+ "load": [0.42, 0.38, 0.31]
287
+ }
288
+ ```
289
+
290
+ ---
291
+
292
+ ### Daemon Management
293
+
294
+ ```bash
295
+ vssh server # Start daemon (foreground)
296
+ vssh install # Install as systemd service (Linux) or launchd (macOS)
297
+ vssh up # Start via systemd
298
+ vssh down # Stop daemon
299
+ vssh restart # Restart daemon
300
+ ```
301
+
302
+ ---
303
+
304
+ ## RPC System
305
+
306
+ RPC is vssh's structured remote call interface. Unlike SSH exec (raw shell), RPC returns typed JSON results — no screen scraping needed.
307
+
308
+ ### Protocol
309
+
310
+ ```
311
+ Client → Server:
312
+ RPC:{token}:{method}:{payload_length}\n
313
+ {JSON payload (payload_length bytes)}
314
+
315
+ Server → Client:
316
+ OK:{response_length}\n
317
+ {JSON response (response_length bytes)}
318
+ __END__
319
+ ```
320
+
321
+ If the method takes no arguments, `payload_length` is `0` and no payload follows.
322
+
323
+ ### CLI Usage
324
+
325
+ ```bash
326
+ vssh rpc <host> <method> # Call with no arguments
327
+ vssh rpc <host> <method> '{"key":"value"}' # Call with JSON payload
328
+ vssh rpc-list <host> # List all available methods
329
+ ```
330
+
331
+ ### RPC Methods Reference
332
+
333
+ #### `get_gpu` — GPU Status
334
+
335
+ ```bash
336
+ vssh rpc g1 get_gpu
337
+ ```
338
+
339
+ **Response:**
340
+ ```json
341
+ {
342
+ "available": true,
343
+ "gpus": [
344
+ {
345
+ "name": "NVIDIA RTX 4090",
346
+ "memory_total": 24576,
347
+ "memory_used": 8192,
348
+ "memory_free": 16384,
349
+ "utilization": 43,
350
+ "temperature": 71
351
+ }
352
+ ]
353
+ }
354
+ ```
355
+
356
+ If no GPU: `{ "available": false, "error": "nvidia-smi failed" }`
357
+
358
+ ---
359
+
360
+ #### `get_disk` — Disk Usage
361
+
362
+ ```bash
363
+ vssh rpc web1 get_disk
364
+ vssh rpc web1 get_disk '{"path": "/data"}'
365
+ ```
366
+
367
+ **Payload:** `{ "path": "/" }` (default: `/`)
368
+
369
+ **Response:**
370
+ ```json
371
+ {
372
+ "path": "/",
373
+ "total": 107374182400,
374
+ "used": 48318382080,
375
+ "free": 59055800320,
376
+ "percent": 45
377
+ }
378
+ ```
379
+
380
+ ---
381
+
382
+ #### `get_memory` — Memory Usage
383
+
384
+ ```bash
385
+ vssh rpc web1 get_memory
386
+ ```
387
+
388
+ **Response:**
389
+ ```json
390
+ {
391
+ "total": 17179869184,
392
+ "used": 4508876800,
393
+ "free": 12670992384,
394
+ "available": 12670992384
395
+ }
396
+ ```
397
+
398
+ All values in bytes.
399
+
400
+ ---
401
+
402
+ #### `get_load` — System Load Average
403
+
404
+ ```bash
405
+ vssh rpc web1 get_load
406
+ ```
407
+
408
+ **Response:**
409
+ ```json
410
+ {
411
+ "load1": 0.42,
412
+ "load5": 0.38,
413
+ "load15": 0.31,
414
+ "processes": "2/412"
415
+ }
416
+ ```
417
+
418
+ ---
419
+
420
+ #### `get_processes` — Top Processes
421
+
422
+ ```bash
423
+ vssh rpc web1 get_processes
424
+ vssh rpc web1 get_processes '{"n": 5, "sort": "mem"}'
425
+ ```
426
+
427
+ **Payload:**
428
+ | Field | Type | Default | Description |
429
+ |---|---|---|---|
430
+ | `n` | int | 10 | Number of processes to return |
431
+ | `sort` | string | `"cpu"` | Sort by `"cpu"` or `"mem"` |
432
+
433
+ **Response:**
434
+ ```json
435
+ {
436
+ "processes": [
437
+ {
438
+ "user": "root",
439
+ "pid": 1234,
440
+ "cpu": 45.2,
441
+ "mem": 2.1,
442
+ "command": "/usr/bin/python3 train.py --epochs 100"
443
+ }
444
+ ]
445
+ }
446
+ ```
447
+
448
+ ---
449
+
450
+ #### `get_logs` — Read Logs
451
+
452
+ ```bash
453
+ vssh rpc web1 get_logs '{"service": "nginx", "lines": 20}'
454
+ vssh rpc web1 get_logs '{"service": "syslog"}'
455
+ vssh rpc web1 get_logs '{"service": "ollama", "lines": 50}'
456
+ ```
457
+
458
+ **Payload:**
459
+ | Field | Type | Default | Description |
460
+ |---|---|---|---|
461
+ | `service` | string | `"syslog"` | Service name or log type |
462
+ | `lines` | int | 50 | Number of lines to return |
463
+
464
+ **Built-in log paths:**
465
+ | Service key | Log file |
466
+ |---|---|
467
+ | `syslog` | `/var/log/syslog` |
468
+ | `auth` | `/var/log/auth.log` |
469
+ | `nginx` | `/var/log/nginx/access.log` |
470
+ | `nginx_error` | `/var/log/nginx/error.log` |
471
+ | `docker` | `/var/log/docker.log` |
472
+ | *(any other)* | `journalctl -u {service} -n {lines}` |
473
+
474
+ **Response:**
475
+ ```json
476
+ {
477
+ "service": "nginx",
478
+ "path": "/var/log/nginx/access.log",
479
+ "lines": ["192.168.1.1 - - [01/Mar/2025:12:00:00 +0000] \"GET /api/health\" 200", "..."]
480
+ }
481
+ ```
482
+
483
+ ---
484
+
485
+ #### `restart_service` — Restart a Service
486
+
487
+ ```bash
488
+ vssh rpc web1 restart_service '{"service": "nginx"}'
489
+ vssh rpc web1 restart_service '{"service": "ollama"}'
490
+ ```
491
+
492
+ **Payload:** `{ "service": "nginx" }`
493
+
494
+ **Allowed services:** `nginx`, `docker`, `ollama`, `chromadb`, `postgresql`, `redis`, `server-agent`
495
+
496
+ **Response:**
497
+ ```json
498
+ { "success": true, "service": "nginx" }
499
+ ```
500
+
501
+ Or on failure:
502
+ ```json
503
+ { "success": false, "error": "Job for nginx.service failed" }
504
+ ```
505
+
506
+ **Permission:** `admin` (requires `VSSH_SECRET` match)
507
+
508
+ ---
509
+
510
+ #### `service_status` — Check Service Status
511
+
512
+ ```bash
513
+ vssh rpc web1 service_status '{"service": "nginx"}'
514
+ ```
515
+
516
+ **Response:**
517
+ ```json
518
+ {
519
+ "service": "nginx",
520
+ "status": "active",
521
+ "active": true
522
+ }
523
+ ```
524
+
525
+ ---
526
+
527
+ #### `list_services` — List Running Services
528
+
529
+ ```bash
530
+ vssh rpc web1 list_services
531
+ ```
532
+
533
+ **Response:**
534
+ ```json
535
+ {
536
+ "services": [
537
+ { "name": "nginx", "load": "loaded", "active": "active", "sub": "running" },
538
+ { "name": "docker", "load": "loaded", "active": "active", "sub": "running" },
539
+ { "name": "vssh", "load": "loaded", "active": "active", "sub": "running" }
540
+ ]
541
+ }
542
+ ```
543
+
544
+ ---
545
+
546
+ #### `docker_containers` — List Docker Containers
547
+
548
+ ```bash
549
+ vssh rpc web1 docker_containers
550
+ ```
551
+
552
+ **Response:**
553
+ ```json
554
+ {
555
+ "containers": [
556
+ { "name": "app", "status": "Up 2 hours", "image": "myapp:latest" },
557
+ { "name": "redis", "status": "Up 5 days", "image": "redis:alpine" }
558
+ ]
559
+ }
560
+ ```
561
+
562
+ ---
563
+
564
+ #### `file_read` — Read a File
565
+
566
+ ```bash
567
+ vssh rpc web1 file_read '{"path": "/etc/nginx/nginx.conf"}'
568
+ ```
569
+
570
+ **Payload:** `{ "path": "/path/to/file" }`
571
+
572
+ **Security:** Blocks access to `/etc/shadow`, `/etc/passwd`, `.ssh/`, `.gnupg/`, paths containing `secret`, `password`, or `credential`. Files over 1MB are rejected.
573
+
574
+ **Response:**
575
+ ```json
576
+ {
577
+ "path": "/etc/nginx/nginx.conf",
578
+ "size": 1024,
579
+ "content": "user nginx;\nworker_processes auto;\n..."
580
+ }
581
+ ```
582
+
583
+ ---
584
+
585
+ #### `file_write` — Write a File
586
+
587
+ ```bash
588
+ vssh rpc web1 file_write '{"path": "/opt/app/config.json", "content": "{\"debug\": false}"}'
589
+ ```
590
+
591
+ **Payload:**
592
+ | Field | Type | Description |
593
+ |---|---|---|
594
+ | `path` | string | Destination path |
595
+ | `content` | string | File content (text) |
596
+
597
+ **Security:** Blocks writing to system paths: `/etc/`, `/usr/`, `/bin/`, `/sbin/`, `/var/log/`, `/root/`.
598
+
599
+ **Response:**
600
+ ```json
601
+ { "success": true, "path": "/opt/app/config.json", "size": 16 }
602
+ ```
603
+
604
+ **Permission:** `write`
605
+
606
+ ---
607
+
608
+ #### `get_network_info` — Network Info
609
+
610
+ ```bash
611
+ vssh rpc web1 get_network_info
612
+ ```
613
+
614
+ **Response:**
615
+ ```json
616
+ {
617
+ "local_ips": ["10.99.1.10", "192.168.1.50"],
618
+ "public_ip": "203.0.113.10"
619
+ }
620
+ ```
621
+
622
+ Used internally for LAN-direct optimization.
623
+
624
+ ---
625
+
626
+ #### `p2p_signal` — P2P Signaling
627
+
628
+ Used internally by `vssh p2p` to coordinate NAT hole-punch. Exchanges external IP/port info between peers via the VPN mesh relay.
629
+
630
+ ```bash
631
+ vssh rpc relay p2p_signal '{"external_ip":"203.0.113.1","external_port":48300,"local_port":48300}'
632
+ ```
633
+
634
+ ---
635
+
636
+ ### RPC via Session
637
+
638
+ You can run multiple RPC calls on a single persistent connection using `vssh session`:
639
+
640
+ ```bash
641
+ vssh session web1
642
+ ```
643
+
644
+ Inside the session:
645
+ ```
646
+ web1> rpc get_disk
647
+ web1> rpc get_memory
648
+ web1> rpc get_load
649
+ web1> rpc restart_service {"service": "nginx"}
650
+ web1> exit
651
+ ```
652
+
653
+ Or programmatically via the Python API:
654
+ ```python
655
+ from vssh import VsshSession
656
+
657
+ with VsshSession("10.99.1.10") as sess:
658
+ disk = sess.rpc("get_disk")
659
+ mem = sess.rpc("get_memory")
660
+ load = sess.rpc("get_load")
661
+ # All on same TCP connection — no reconnect overhead
662
+ ```
663
+
664
+ ---
665
+
666
+ ## Before / After: Managing a Fleet
667
+
668
+ ### Before (SSH)
669
+
670
+ ```bash
671
+ # Deploy new config to 10 servers
672
+ for ip in 10.0.1.1 10.0.1.2 10.0.1.3 10.0.1.4 10.0.1.5 \
673
+ 10.0.1.6 10.0.1.7 10.0.1.8 10.0.1.9 10.0.1.10; do
674
+ scp -i ~/.ssh/id_rsa ./nginx.conf root@$ip:/etc/nginx/nginx.conf
675
+ ssh -i ~/.ssh/id_rsa root@$ip "nginx -t && systemctl reload nginx"
676
+ done
677
+ ```
678
+
679
+ Problems: must track IPs manually, full SSH handshake on every connection (~200-400ms each), key management across all nodes, no unified view of which nodes are reachable.
680
+
681
+ ### After (vssh)
682
+
683
+ ```bash
684
+ # Deploy new config to 10 servers
685
+ for node in web{1..10}; do
686
+ vssh put $node ./nginx.conf /etc/nginx/nginx.conf
687
+ vssh exec $node "nginx -t && systemctl reload nginx"
688
+ done
689
+ ```
690
+
691
+ Benefits: node names auto-discovered from wire mesh, persistent daemon — connections are instant, one shared secret across all nodes, `vssh status` shows full fleet health at a glance.
692
+
693
+ ---
694
+
695
+ ## Connection Speed
696
+
697
+ vssh is measurably faster than SSH for repeated commands because:
698
+
699
+ | | SSH | vssh |
700
+ |---|---|---|
701
+ | Connection | TCP handshake + key exchange each time | Persistent daemon, HMAC auth only |
702
+ | Auth | RSA/Ed25519 challenge-response | HMAC-SHA256 token (microseconds) |
703
+ | Transport | TCP | UDP (WireGuard tunnel) |
704
+ | Name resolution | `/etc/hosts` or DNS | Wire mesh coordinator (auto) |
705
+ | Failover | Manual | Auto-failover to Tailscale |
706
+
707
+ ---
708
+
709
+ ## Auto-Discovery
710
+
711
+ When used with [wire](https://github.com/meshpop/wire) mesh VPN, vssh reads the wire coordinator URL from `/etc/wire/config.json` and queries `/peers` for the current node list — no config file needed.
712
+
713
+ ```bash
714
+ # After wire is set up, vssh knows all nodes automatically
715
+ vssh status # shows all mesh nodes immediately
716
+ ```
717
+
718
+ For standalone use without wire, create `~/.vssh/config`:
719
+
720
+ ```ini
721
+ web1=192.168.1.10
722
+ web2=192.168.1.11
723
+ web3=192.168.1.12
724
+ SECRET=your-shared-secret
725
+ ```
726
+
727
+ ---
728
+
729
+ ## Tailscale Failover
730
+
731
+ vssh automatically detects your Tailscale peers and builds a fallback map (`~/.vssh/tailscale_map`). If a node's primary wire IP is unreachable, vssh transparently retries via Tailscale — no intervention needed.
732
+
733
+ The map is auto-generated on first run (by cross-referencing `tailscale status --json` with wire `/peers`) and refreshed every 24 hours.
734
+
735
+ Failover logic:
736
+ 1. Try Wire VPN IP (2s fast path)
737
+ 2. If timeout: wait 300ms, retry once (WireGuard handshake may be completing)
738
+ 3. If still failing: switch to Tailscale IP, cache for 60s
739
+ 4. After 60s: retry Wire — if recovered, clear failover cache
740
+
741
+ You can also configure the mapping manually in `~/.vssh/config`:
742
+ ```ini
743
+ # TAILSCALE.<wire_ip> = <tailscale_ip>
744
+ TAILSCALE.10.99.1.10 = 100.64.0.1
745
+ TAILSCALE.10.99.1.11 = 100.64.0.2
746
+ ```
747
+
748
+ ---
749
+
750
+ ## MCP Integration (AI Agents)
751
+
752
+ vssh ships with an MCP server, letting AI agents (Claude, etc.) execute commands and transfer files across your fleet.
753
+
754
+ ```json
755
+ {
756
+ "mcpServers": {
757
+ "vssh": { "command": "vssh-mcp" }
758
+ }
759
+ }
760
+ ```
761
+
762
+ ### MCP Tools Reference
763
+
764
+ #### `vssh_status` — Fleet Status
765
+
766
+ Check connection status to all configured servers.
767
+
768
+ ```json
769
+ // Input: (none)
770
+
771
+ // Output:
772
+ {
773
+ "summary": "3/3 online",
774
+ "servers": {
775
+ "web1": { "status": "online", "ip": "10.99.1.10", "latency_ms": 8.2 },
776
+ "web2": { "status": "online", "ip": "10.99.1.11", "latency_ms": 9.1 },
777
+ "db1": { "status": "offline", "ip": "10.99.1.20" }
778
+ }
779
+ }
780
+ ```
781
+
782
+ ---
783
+
784
+ #### `vssh_exec` — Execute Command
785
+
786
+ Run a shell command on a remote server.
787
+
788
+ ```json
789
+ // Input:
790
+ {
791
+ "server": "web1",
792
+ "command": "systemctl restart nginx && systemctl is-active nginx"
793
+ }
794
+
795
+ // Output:
796
+ {
797
+ "server": "web1",
798
+ "command": "systemctl restart nginx && systemctl is-active nginx",
799
+ "output": "active"
800
+ }
801
+ ```
802
+
803
+ ---
804
+
805
+ #### `vssh_put` — Upload File
806
+
807
+ Upload a local file to a remote server.
808
+
809
+ ```json
810
+ // Input:
811
+ {
812
+ "server": "web1",
813
+ "local_path": "/tmp/app.tar.gz",
814
+ "remote_path": "/opt/app.tar.gz"
815
+ }
816
+
817
+ // Output:
818
+ {
819
+ "status": "success",
820
+ "size": 52428800,
821
+ "speed_mbps": 54.2,
822
+ "elapsed": 0.97,
823
+ "remote_path": "/opt/app.tar.gz"
824
+ }
825
+ ```
826
+
827
+ ---
828
+
829
+ #### `vssh_get` — Download File
830
+
831
+ Download a file from a remote server.
832
+
833
+ ```json
834
+ // Input:
835
+ {
836
+ "server": "web1",
837
+ "remote_path": "/var/log/app.log",
838
+ "local_path": "/tmp/app.log"
839
+ }
840
+
841
+ // Output:
842
+ {
843
+ "status": "success",
844
+ "size": 1048576,
845
+ "speed_mbps": 48.3,
846
+ "elapsed": 0.02,
847
+ "local_path": "/tmp/app.log"
848
+ }
849
+ ```
850
+
851
+ ---
852
+
853
+ #### `vssh_sync` — Sync Directory
854
+
855
+ Sync a directory from one server to another via local relay (download + re-upload).
856
+
857
+ ```json
858
+ // Input:
859
+ {
860
+ "source": "web1",
861
+ "dest": "web2",
862
+ "path": "/etc/app"
863
+ }
864
+
865
+ // Output:
866
+ {
867
+ "source": "web1",
868
+ "dest": "web2",
869
+ "path": "/etc/app",
870
+ "total_files": 12,
871
+ "synced": 12,
872
+ "failed": 0,
873
+ "errors": []
874
+ }
875
+ ```
876
+
877
+ ---
878
+
879
+ #### `vssh_speed_test` — Transfer Speed Test
880
+
881
+ Test upload and download speed to a server.
882
+
883
+ ```json
884
+ // Input:
885
+ {
886
+ "server": "web1",
887
+ "size_mb": 10
888
+ }
889
+
890
+ // Output:
891
+ {
892
+ "server": "web1",
893
+ "size_mb": 10,
894
+ "upload_mbps": 54.2,
895
+ "download_mbps": 48.3,
896
+ "latency_ms": 9.1
897
+ }
898
+ ```
899
+
900
+ ---
901
+
902
+ #### `vssh_p2p_status` — P2P Status
903
+
904
+ Check NAT hole-punch capability.
905
+
906
+ ```json
907
+ // Output:
908
+ {
909
+ "p2p_available": true,
910
+ "external_ip": "203.0.113.1",
911
+ "external_port": 48310,
912
+ "nat_type": "cone"
913
+ }
914
+ ```
915
+
916
+ ---
917
+
918
+ #### `vssh_tunnel` — SSH Tunnel (Port Forwarding)
919
+
920
+ Create an SSH tunnel to access a remote service locally.
921
+
922
+ ```json
923
+ // Input:
924
+ {
925
+ "server": "db1",
926
+ "local_port": 5433,
927
+ "remote_port": 5432,
928
+ "remote_host": "localhost"
929
+ }
930
+
931
+ // Output:
932
+ {
933
+ "status": "running",
934
+ "pid": 12345,
935
+ "server": "db1",
936
+ "local_port": 5433,
937
+ "remote_host": "localhost",
938
+ "remote_port": 5432,
939
+ "access": "localhost:5433",
940
+ "usage": "Connect to localhost:5432 on db1 via localhost:5433",
941
+ "close_cmd": "kill 12345"
942
+ }
943
+ ```
944
+
945
+ ---
946
+
947
+ #### `vssh_keys` — Key Management
948
+
949
+ Show configured secrets and available servers.
950
+
951
+ ```json
952
+ // Output:
953
+ {
954
+ "secret_configured": true,
955
+ "secret_preview": "a1b2c3d4...",
956
+ "key_directory": "/Users/you/.vssh",
957
+ "keys": [
958
+ { "name": "secret", "size": 32, "permissions": "600" },
959
+ { "name": "tailscale_map", "size": 412, "permissions": "644" }
960
+ ],
961
+ "servers_with_vssh": ["web1", "web2", "web3"]
962
+ }
963
+ ```
964
+
965
+ ---
966
+
967
+ ### MCP Usage Examples
968
+
969
+ **Fleet health check:**
970
+ ```
971
+ vssh_status → check all servers
972
+ vssh_exec(web1, "df -h / | tail -1") → check disk
973
+ vssh_exec(web1, "free -m | grep Mem") → check memory
974
+ ```
975
+
976
+ **Deploy update across fleet:**
977
+ ```
978
+ vssh_put(web1, "/tmp/app-v2.tar.gz", "/opt/app.tar.gz")
979
+ vssh_exec(web1, "cd /opt && tar xf app.tar.gz && systemctl restart app")
980
+ vssh_exec(web1, "systemctl is-active app")
981
+ ```
982
+
983
+ **Access database locally via tunnel:**
984
+ ```
985
+ vssh_tunnel(db1, local_port=5433, remote_port=5432)
986
+ → Connect your DB client to localhost:5433
987
+ ```
988
+
989
+ ---
990
+
991
+ ## Architecture
992
+
993
+ ```
994
+ ┌─────────────────────────────────────────────┐
995
+ │ Your Fleet │
996
+ │ │
997
+ │ ┌──────┐ ┌──────┐ ┌──────┐ │
998
+ │ │ web1 │ │ web2 │ │ web3 │ ... │
999
+ │ │vsshd │ │vsshd │ │vsshd │ │
1000
+ │ └──┬───┘ └──┬───┘ └──┬───┘ │
1001
+ │ └───────────┴───────────┘ │
1002
+ │ WireGuard mesh │
1003
+ │ (wire coordinator) │
1004
+ └──────────────────┬──────────────────────────┘
1005
+
1006
+ ┌──────┴──────┐
1007
+ │ vssh client │ ← your machine
1008
+ │ (Mac/Linux) │
1009
+ └─────────────┘
1010
+ ```
1011
+
1012
+ vssh sits at **Layer 2 (Transport)** of the MeshPOP stack:
1013
+
1014
+ - **Layer 1** — [wire](https://github.com/meshpop/wire): WireGuard mesh VPN
1015
+ - **Layer 2** — vssh: authenticated transport (this project)
1016
+ - **Layer 3** — mpop: AI agent orchestration
1017
+
1018
+ Each layer is independently installable and usable on its own.
1019
+
1020
+ ---
1021
+
1022
+ ## Links
1023
+
1024
+ - Wire VPN: [github.com/meshpop/wire](https://github.com/meshpop/wire)
1025
+ - Main project: [github.com/meshpop/mpop](https://github.com/meshpop/mpop)
1026
+ - PyPI: [pypi.org/project/vssh](https://pypi.org/project/vssh/)
1027
+
1028
+ ## License
1029
+
1030
+ MIT