vssh 3.7.2__tar.gz → 3.7.4__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.4/PKG-INFO ADDED
@@ -0,0 +1,1002 @@
1
+ Metadata-Version: 2.4
2
+ Name: vssh
3
+ Version: 3.7.4
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.4 - 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.4 - 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 rsync <local> <host>:<remote> # Delta sync (only changed blocks)
217
+ ```
218
+
219
+ **Transfer flags:**
220
+ | Flag | Description |
221
+ |---|---|
222
+ | `-z` / `--compress` | Force zlib compression |
223
+ | `--no-compress` | Disable auto-compression |
224
+ | `--resume` | Resume interrupted transfer (for files >100MB) |
225
+ | `--retry=N` | Retry N times on network failure |
226
+ | `--limit=X` | Rate limit (e.g. `--limit=10MB`) |
227
+ | `--no-lan` | Disable LAN-direct optimization |
228
+
229
+ **Examples:**
230
+ ```bash
231
+ vssh put ./app.tar.gz web1:/opt/app.tar.gz # Upload
232
+ vssh get web1:/var/log/app.log ./app.log # Download
233
+ vssh sync ./config/ web1:/etc/app/ # Sync directory
234
+ vssh mput web1:/backup/ file1.txt file2.txt file3.log # Multi-file
235
+ vssh rsync ./src/ web1:/opt/src/ # Delta sync (only changes)
236
+ ```
237
+
238
+ vssh auto-compresses text files (`.py`, `.js`, `.json`, `.log`, `.md`, etc.) and skips upload for files with matching MD5.
239
+
240
+ ---
241
+
242
+ ### Speed Test
243
+
244
+ ```bash
245
+ vssh speed-test <host> # Transfer speed test (10MB default)
246
+ vssh speed-test <host> --size=50 # Custom size in MB
247
+ ```
248
+
249
+ ```
250
+ → 54.2 MB/s (WireGuard mesh, direct)
251
+ ```
252
+
253
+ ---
254
+
255
+ ### Pipes (stdin/stdout)
256
+
257
+ ```bash
258
+ # Pipe stdin to remote file
259
+ cat local.log | vssh pipe-up web1:/var/log/backup.log
260
+
261
+ # Pipe remote command to stdout
262
+ vssh pipe-down web1:"journalctl -u nginx -n 100" | grep ERROR
263
+
264
+ # Auto-detect: stdin present → upload, otherwise → download
265
+ cat data.csv | vssh pipe web1:/data/input.csv
266
+ vssh pipe web1:"cat /etc/os-release" > os-info.txt
267
+ ```
268
+
269
+ ---
270
+
271
+ ### Node Info
272
+
273
+ ```bash
274
+ vssh info <host> # Full JSON node info (OS, IPs, vsshd version, etc.)
275
+ ```
276
+
277
+ ```json
278
+ {
279
+ "hostname": "web1",
280
+ "os": "Linux-5.15.0",
281
+ "vssh_version": "3.7.2",
282
+ "local_ips": ["10.99.1.10"],
283
+ "public_ip": "203.0.113.10",
284
+ "load": [0.42, 0.38, 0.31]
285
+ }
286
+ ```
287
+
288
+ ---
289
+
290
+ ### Daemon Management
291
+
292
+ ```bash
293
+ vssh server # Start daemon (foreground)
294
+ vssh install # Install as systemd service (Linux) or launchd (macOS)
295
+ vssh up # Start via systemd
296
+ vssh down # Stop daemon
297
+ vssh restart # Restart daemon
298
+ ```
299
+
300
+ ---
301
+
302
+ ## RPC System
303
+
304
+ RPC is vssh's structured remote call interface. Unlike SSH exec (raw shell), RPC returns typed JSON results — no screen scraping needed.
305
+
306
+ ### Protocol
307
+
308
+ ```
309
+ Client → Server:
310
+ RPC:{token}:{method}:{payload_length}\n
311
+ {JSON payload (payload_length bytes)}
312
+
313
+ Server → Client:
314
+ OK:{response_length}\n
315
+ {JSON response (response_length bytes)}
316
+ __END__
317
+ ```
318
+
319
+ If the method takes no arguments, `payload_length` is `0` and no payload follows.
320
+
321
+ ### CLI Usage
322
+
323
+ ```bash
324
+ vssh rpc <host> <method> # Call with no arguments
325
+ vssh rpc <host> <method> '{"key":"value"}' # Call with JSON payload
326
+ vssh rpc-list <host> # List all available methods
327
+ ```
328
+
329
+ ### RPC Methods Reference
330
+
331
+ #### `get_gpu` — GPU Status
332
+
333
+ ```bash
334
+ vssh rpc g1 get_gpu
335
+ ```
336
+
337
+ **Response:**
338
+ ```json
339
+ {
340
+ "available": true,
341
+ "gpus": [
342
+ {
343
+ "name": "NVIDIA RTX 4090",
344
+ "memory_total": 24576,
345
+ "memory_used": 8192,
346
+ "memory_free": 16384,
347
+ "utilization": 43,
348
+ "temperature": 71
349
+ }
350
+ ]
351
+ }
352
+ ```
353
+
354
+ If no GPU: `{ "available": false, "error": "nvidia-smi failed" }`
355
+
356
+ ---
357
+
358
+ #### `get_disk` — Disk Usage
359
+
360
+ ```bash
361
+ vssh rpc web1 get_disk
362
+ vssh rpc web1 get_disk '{"path": "/data"}'
363
+ ```
364
+
365
+ **Payload:** `{ "path": "/" }` (default: `/`)
366
+
367
+ **Response:**
368
+ ```json
369
+ {
370
+ "path": "/",
371
+ "total": 107374182400,
372
+ "used": 48318382080,
373
+ "free": 59055800320,
374
+ "percent": 45
375
+ }
376
+ ```
377
+
378
+ ---
379
+
380
+ #### `get_memory` — Memory Usage
381
+
382
+ ```bash
383
+ vssh rpc web1 get_memory
384
+ ```
385
+
386
+ **Response:**
387
+ ```json
388
+ {
389
+ "total": 17179869184,
390
+ "used": 4508876800,
391
+ "free": 12670992384,
392
+ "available": 12670992384
393
+ }
394
+ ```
395
+
396
+ All values in bytes.
397
+
398
+ ---
399
+
400
+ #### `get_load` — System Load Average
401
+
402
+ ```bash
403
+ vssh rpc web1 get_load
404
+ ```
405
+
406
+ **Response:**
407
+ ```json
408
+ {
409
+ "load1": 0.42,
410
+ "load5": 0.38,
411
+ "load15": 0.31,
412
+ "processes": "2/412"
413
+ }
414
+ ```
415
+
416
+ ---
417
+
418
+ #### `get_processes` — Top Processes
419
+
420
+ ```bash
421
+ vssh rpc web1 get_processes
422
+ vssh rpc web1 get_processes '{"n": 5, "sort": "mem"}'
423
+ ```
424
+
425
+ **Payload:**
426
+ | Field | Type | Default | Description |
427
+ |---|---|---|---|
428
+ | `n` | int | 10 | Number of processes to return |
429
+ | `sort` | string | `"cpu"` | Sort by `"cpu"` or `"mem"` |
430
+
431
+ **Response:**
432
+ ```json
433
+ {
434
+ "processes": [
435
+ {
436
+ "user": "root",
437
+ "pid": 1234,
438
+ "cpu": 45.2,
439
+ "mem": 2.1,
440
+ "command": "/usr/bin/python3 train.py --epochs 100"
441
+ }
442
+ ]
443
+ }
444
+ ```
445
+
446
+ ---
447
+
448
+ #### `get_logs` — Read Logs
449
+
450
+ ```bash
451
+ vssh rpc web1 get_logs '{"service": "nginx", "lines": 20}'
452
+ vssh rpc web1 get_logs '{"service": "syslog"}'
453
+ vssh rpc web1 get_logs '{"service": "ollama", "lines": 50}'
454
+ ```
455
+
456
+ **Payload:**
457
+ | Field | Type | Default | Description |
458
+ |---|---|---|---|
459
+ | `service` | string | `"syslog"` | Service name or log type |
460
+ | `lines` | int | 50 | Number of lines to return |
461
+
462
+ **Built-in log paths:**
463
+ | Service key | Log file |
464
+ |---|---|
465
+ | `syslog` | `/var/log/syslog` |
466
+ | `auth` | `/var/log/auth.log` |
467
+ | `nginx` | `/var/log/nginx/access.log` |
468
+ | `nginx_error` | `/var/log/nginx/error.log` |
469
+ | `docker` | `/var/log/docker.log` |
470
+ | *(any other)* | `journalctl -u {service} -n {lines}` |
471
+
472
+ **Response:**
473
+ ```json
474
+ {
475
+ "service": "nginx",
476
+ "path": "/var/log/nginx/access.log",
477
+ "lines": ["192.168.1.1 - - [01/Mar/2025:12:00:00 +0000] \"GET /api/health\" 200", "..."]
478
+ }
479
+ ```
480
+
481
+ ---
482
+
483
+ #### `restart_service` — Restart a Service
484
+
485
+ ```bash
486
+ vssh rpc web1 restart_service '{"service": "nginx"}'
487
+ vssh rpc web1 restart_service '{"service": "ollama"}'
488
+ ```
489
+
490
+ **Payload:** `{ "service": "nginx" }`
491
+
492
+ **Allowed services:** `nginx`, `docker`, `ollama`, `chromadb`, `postgresql`, `redis`, `server-agent`
493
+
494
+ **Response:**
495
+ ```json
496
+ { "success": true, "service": "nginx" }
497
+ ```
498
+
499
+ Or on failure:
500
+ ```json
501
+ { "success": false, "error": "Job for nginx.service failed" }
502
+ ```
503
+
504
+ **Permission:** `admin` (requires `VSSH_SECRET` match)
505
+
506
+ ---
507
+
508
+ #### `service_status` — Check Service Status
509
+
510
+ ```bash
511
+ vssh rpc web1 service_status '{"service": "nginx"}'
512
+ ```
513
+
514
+ **Response:**
515
+ ```json
516
+ {
517
+ "service": "nginx",
518
+ "status": "active",
519
+ "active": true
520
+ }
521
+ ```
522
+
523
+ ---
524
+
525
+ #### `list_services` — List Running Services
526
+
527
+ ```bash
528
+ vssh rpc web1 list_services
529
+ ```
530
+
531
+ **Response:**
532
+ ```json
533
+ {
534
+ "services": [
535
+ { "name": "nginx", "load": "loaded", "active": "active", "sub": "running" },
536
+ { "name": "docker", "load": "loaded", "active": "active", "sub": "running" },
537
+ { "name": "vssh", "load": "loaded", "active": "active", "sub": "running" }
538
+ ]
539
+ }
540
+ ```
541
+
542
+ ---
543
+
544
+ #### `docker_containers` — List Docker Containers
545
+
546
+ ```bash
547
+ vssh rpc web1 docker_containers
548
+ ```
549
+
550
+ **Response:**
551
+ ```json
552
+ {
553
+ "containers": [
554
+ { "name": "app", "status": "Up 2 hours", "image": "myapp:latest" },
555
+ { "name": "redis", "status": "Up 5 days", "image": "redis:alpine" }
556
+ ]
557
+ }
558
+ ```
559
+
560
+ ---
561
+
562
+ #### `file_read` — Read a File
563
+
564
+ ```bash
565
+ vssh rpc web1 file_read '{"path": "/etc/nginx/nginx.conf"}'
566
+ ```
567
+
568
+ **Payload:** `{ "path": "/path/to/file" }`
569
+
570
+ **Security:** Blocks access to `/etc/shadow`, `/etc/passwd`, `.ssh/`, `.gnupg/`, paths containing `secret`, `password`, or `credential`. Files over 1MB are rejected.
571
+
572
+ **Response:**
573
+ ```json
574
+ {
575
+ "path": "/etc/nginx/nginx.conf",
576
+ "size": 1024,
577
+ "content": "user nginx;\nworker_processes auto;\n..."
578
+ }
579
+ ```
580
+
581
+ ---
582
+
583
+ #### `file_write` — Write a File
584
+
585
+ ```bash
586
+ vssh rpc web1 file_write '{"path": "/opt/app/config.json", "content": "{\"debug\": false}"}'
587
+ ```
588
+
589
+ **Payload:**
590
+ | Field | Type | Description |
591
+ |---|---|---|
592
+ | `path` | string | Destination path |
593
+ | `content` | string | File content (text) |
594
+
595
+ **Security:** Blocks writing to system paths: `/etc/`, `/usr/`, `/bin/`, `/sbin/`, `/var/log/`, `/root/`.
596
+
597
+ **Response:**
598
+ ```json
599
+ { "success": true, "path": "/opt/app/config.json", "size": 16 }
600
+ ```
601
+
602
+ **Permission:** `write`
603
+
604
+ ---
605
+
606
+ #### `get_network_info` — Network Info
607
+
608
+ ```bash
609
+ vssh rpc web1 get_network_info
610
+ ```
611
+
612
+ **Response:**
613
+ ```json
614
+ {
615
+ "local_ips": ["10.99.1.10", "192.168.1.50"],
616
+ "public_ip": "203.0.113.10"
617
+ }
618
+ ```
619
+
620
+ Used internally for LAN-direct optimization.
621
+
622
+ ---
623
+
624
+ ### RPC via Session
625
+
626
+ You can run multiple RPC calls on a single persistent connection using `vssh session`:
627
+
628
+ ```bash
629
+ vssh session web1
630
+ ```
631
+
632
+ Inside the session:
633
+ ```
634
+ web1> rpc get_disk
635
+ web1> rpc get_memory
636
+ web1> rpc get_load
637
+ web1> rpc restart_service {"service": "nginx"}
638
+ web1> exit
639
+ ```
640
+
641
+ Or programmatically via the Python API:
642
+ ```python
643
+ from vssh import VsshSession
644
+
645
+ with VsshSession("10.99.1.10") as sess:
646
+ disk = sess.rpc("get_disk")
647
+ mem = sess.rpc("get_memory")
648
+ load = sess.rpc("get_load")
649
+ # All on same TCP connection — no reconnect overhead
650
+ ```
651
+
652
+ ---
653
+
654
+ ## Before / After: Managing a Fleet
655
+
656
+ ### Before (SSH)
657
+
658
+ ```bash
659
+ # Deploy new config to 10 servers
660
+ for ip in 10.0.1.1 10.0.1.2 10.0.1.3 10.0.1.4 10.0.1.5 \
661
+ 10.0.1.6 10.0.1.7 10.0.1.8 10.0.1.9 10.0.1.10; do
662
+ scp -i ~/.ssh/id_rsa ./nginx.conf root@$ip:/etc/nginx/nginx.conf
663
+ ssh -i ~/.ssh/id_rsa root@$ip "nginx -t && systemctl reload nginx"
664
+ done
665
+ ```
666
+
667
+ 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.
668
+
669
+ ### After (vssh)
670
+
671
+ ```bash
672
+ # Deploy new config to 10 servers
673
+ for node in web{1..10}; do
674
+ vssh put $node ./nginx.conf /etc/nginx/nginx.conf
675
+ vssh exec $node "nginx -t && systemctl reload nginx"
676
+ done
677
+ ```
678
+
679
+ 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.
680
+
681
+ ---
682
+
683
+ ## Connection Speed
684
+
685
+ vssh is measurably faster than SSH for repeated commands because:
686
+
687
+ | | SSH | vssh |
688
+ |---|---|---|
689
+ | Connection | TCP handshake + key exchange each time | Persistent daemon, HMAC auth only |
690
+ | Auth | RSA/Ed25519 challenge-response | HMAC-SHA256 token (microseconds) |
691
+ | Transport | TCP | UDP (WireGuard tunnel) |
692
+ | Name resolution | `/etc/hosts` or DNS | Wire mesh coordinator (auto) |
693
+ | Failover | Manual | Auto-failover to Tailscale |
694
+
695
+ ---
696
+
697
+ ## Auto-Discovery
698
+
699
+ 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.
700
+
701
+ ```bash
702
+ # After wire is set up, vssh knows all nodes automatically
703
+ vssh status # shows all mesh nodes immediately
704
+ ```
705
+
706
+ For standalone use without wire, create `~/.vssh/config`:
707
+
708
+ ```ini
709
+ web1=192.168.1.10
710
+ web2=192.168.1.11
711
+ web3=192.168.1.12
712
+ SECRET=your-shared-secret
713
+ ```
714
+
715
+ ---
716
+
717
+ ## Tailscale Failover
718
+
719
+ 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.
720
+
721
+ The map is auto-generated on first run (by cross-referencing `tailscale status --json` with wire `/peers`) and refreshed every 24 hours.
722
+
723
+ Failover logic:
724
+ 1. Try Wire VPN IP (2s fast path)
725
+ 2. If timeout: wait 300ms, retry once (WireGuard handshake may be completing)
726
+ 3. If still failing: switch to Tailscale IP, cache for 60s
727
+ 4. After 60s: retry Wire — if recovered, clear failover cache
728
+
729
+ You can also configure the mapping manually in `~/.vssh/config`:
730
+ ```ini
731
+ # TAILSCALE.<wire_ip> = <tailscale_ip>
732
+ TAILSCALE.10.99.1.10 = 100.64.0.1
733
+ TAILSCALE.10.99.1.11 = 100.64.0.2
734
+ ```
735
+
736
+ ---
737
+
738
+ ## MCP Integration (AI Agents)
739
+
740
+ vssh ships with an MCP server, letting AI agents (Claude, etc.) execute commands and transfer files across your fleet.
741
+
742
+ ```json
743
+ {
744
+ "mcpServers": {
745
+ "vssh": { "command": "vssh-mcp" }
746
+ }
747
+ }
748
+ ```
749
+
750
+ ### MCP Tools Reference
751
+
752
+ #### `vssh_status` — Fleet Status
753
+
754
+ Check connection status to all configured servers.
755
+
756
+ ```json
757
+ // Input: (none)
758
+
759
+ // Output:
760
+ {
761
+ "summary": "3/3 online",
762
+ "servers": {
763
+ "web1": { "status": "online", "ip": "10.99.1.10", "latency_ms": 8.2 },
764
+ "web2": { "status": "online", "ip": "10.99.1.11", "latency_ms": 9.1 },
765
+ "db1": { "status": "offline", "ip": "10.99.1.20" }
766
+ }
767
+ }
768
+ ```
769
+
770
+ ---
771
+
772
+ #### `vssh_exec` — Execute Command
773
+
774
+ Run a shell command on a remote server.
775
+
776
+ ```json
777
+ // Input:
778
+ {
779
+ "server": "web1",
780
+ "command": "systemctl restart nginx && systemctl is-active nginx"
781
+ }
782
+
783
+ // Output:
784
+ {
785
+ "server": "web1",
786
+ "command": "systemctl restart nginx && systemctl is-active nginx",
787
+ "output": "active"
788
+ }
789
+ ```
790
+
791
+ ---
792
+
793
+ #### `vssh_put` — Upload File
794
+
795
+ Upload a local file to a remote server.
796
+
797
+ ```json
798
+ // Input:
799
+ {
800
+ "server": "web1",
801
+ "local_path": "/tmp/app.tar.gz",
802
+ "remote_path": "/opt/app.tar.gz"
803
+ }
804
+
805
+ // Output:
806
+ {
807
+ "status": "success",
808
+ "size": 52428800,
809
+ "speed_mbps": 54.2,
810
+ "elapsed": 0.97,
811
+ "remote_path": "/opt/app.tar.gz"
812
+ }
813
+ ```
814
+
815
+ ---
816
+
817
+ #### `vssh_get` — Download File
818
+
819
+ Download a file from a remote server.
820
+
821
+ ```json
822
+ // Input:
823
+ {
824
+ "server": "web1",
825
+ "remote_path": "/var/log/app.log",
826
+ "local_path": "/tmp/app.log"
827
+ }
828
+
829
+ // Output:
830
+ {
831
+ "status": "success",
832
+ "size": 1048576,
833
+ "speed_mbps": 48.3,
834
+ "elapsed": 0.02,
835
+ "local_path": "/tmp/app.log"
836
+ }
837
+ ```
838
+
839
+ ---
840
+
841
+ #### `vssh_sync` — Sync Directory
842
+
843
+ Sync a directory from one server to another via local relay (download + re-upload).
844
+
845
+ ```json
846
+ // Input:
847
+ {
848
+ "source": "web1",
849
+ "dest": "web2",
850
+ "path": "/etc/app"
851
+ }
852
+
853
+ // Output:
854
+ {
855
+ "source": "web1",
856
+ "dest": "web2",
857
+ "path": "/etc/app",
858
+ "total_files": 12,
859
+ "synced": 12,
860
+ "failed": 0,
861
+ "errors": []
862
+ }
863
+ ```
864
+
865
+ ---
866
+
867
+ #### `vssh_speed_test` — Transfer Speed Test
868
+
869
+ Test upload and download speed to a server.
870
+
871
+ ```json
872
+ // Input:
873
+ {
874
+ "server": "web1",
875
+ "size_mb": 10
876
+ }
877
+
878
+ // Output:
879
+ {
880
+ "server": "web1",
881
+ "size_mb": 10,
882
+ "upload_mbps": 54.2,
883
+ "download_mbps": 48.3,
884
+ "latency_ms": 9.1
885
+ }
886
+ ```
887
+
888
+ ---
889
+
890
+ #### `vssh_tunnel` — SSH Tunnel (Port Forwarding)
891
+
892
+ Create an SSH tunnel to access a remote service locally.
893
+
894
+ ```json
895
+ // Input:
896
+ {
897
+ "server": "db1",
898
+ "local_port": 5433,
899
+ "remote_port": 5432,
900
+ "remote_host": "localhost"
901
+ }
902
+
903
+ // Output:
904
+ {
905
+ "status": "running",
906
+ "pid": 12345,
907
+ "server": "db1",
908
+ "local_port": 5433,
909
+ "remote_host": "localhost",
910
+ "remote_port": 5432,
911
+ "access": "localhost:5433",
912
+ "usage": "Connect to localhost:5432 on db1 via localhost:5433",
913
+ "close_cmd": "kill 12345"
914
+ }
915
+ ```
916
+
917
+ ---
918
+
919
+ #### `vssh_keys` — Key Management
920
+
921
+ Show configured secrets and available servers.
922
+
923
+ ```json
924
+ // Output:
925
+ {
926
+ "secret_configured": true,
927
+ "secret_preview": "a1b2c3d4...",
928
+ "key_directory": "/Users/you/.vssh",
929
+ "keys": [
930
+ { "name": "secret", "size": 32, "permissions": "600" },
931
+ { "name": "tailscale_map", "size": 412, "permissions": "644" }
932
+ ],
933
+ "servers_with_vssh": ["web1", "web2", "web3"]
934
+ }
935
+ ```
936
+
937
+ ---
938
+
939
+ ### MCP Usage Examples
940
+
941
+ **Fleet health check:**
942
+ ```
943
+ vssh_status → check all servers
944
+ vssh_exec(web1, "df -h / | tail -1") → check disk
945
+ vssh_exec(web1, "free -m | grep Mem") → check memory
946
+ ```
947
+
948
+ **Deploy update across fleet:**
949
+ ```
950
+ vssh_put(web1, "/tmp/app-v2.tar.gz", "/opt/app.tar.gz")
951
+ vssh_exec(web1, "cd /opt && tar xf app.tar.gz && systemctl restart app")
952
+ vssh_exec(web1, "systemctl is-active app")
953
+ ```
954
+
955
+ **Access database locally via tunnel:**
956
+ ```
957
+ vssh_tunnel(db1, local_port=5433, remote_port=5432)
958
+ → Connect your DB client to localhost:5433
959
+ ```
960
+
961
+ ---
962
+
963
+ ## Architecture
964
+
965
+ ```
966
+ ┌─────────────────────────────────────────────┐
967
+ │ Your Fleet │
968
+ │ │
969
+ │ ┌──────┐ ┌──────┐ ┌──────┐ │
970
+ │ │ web1 │ │ web2 │ │ web3 │ ... │
971
+ │ │vsshd │ │vsshd │ │vsshd │ │
972
+ │ └──┬───┘ └──┬───┘ └──┬───┘ │
973
+ │ └───────────┴───────────┘ │
974
+ │ WireGuard mesh │
975
+ │ (wire coordinator) │
976
+ └──────────────────┬──────────────────────────┘
977
+
978
+ ┌──────┴──────┐
979
+ │ vssh client │ ← your machine
980
+ │ (Mac/Linux) │
981
+ └─────────────┘
982
+ ```
983
+
984
+ vssh sits at **Layer 2 (Transport)** of the MeshPOP stack:
985
+
986
+ - **Layer 1** — [wire](https://github.com/meshpop/wire): WireGuard mesh VPN
987
+ - **Layer 2** — vssh: authenticated transport (this project)
988
+ - **Layer 3** — mpop: AI agent orchestration
989
+
990
+ Each layer is independently installable and usable on its own.
991
+
992
+ ---
993
+
994
+ ## Links
995
+
996
+ - Wire VPN: [github.com/meshpop/wire](https://github.com/meshpop/wire)
997
+ - Main project: [github.com/meshpop/mpop](https://github.com/meshpop/mpop)
998
+ - PyPI: [pypi.org/project/vssh](https://pypi.org/project/vssh/)
999
+
1000
+ ## License
1001
+
1002
+ MIT