vssh 3.3.1__tar.gz → 3.3.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vssh
3
- Version: 3.3.1
3
+ Version: 3.3.6
4
4
  Summary: Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server
5
5
  Author-email: MeshPOP <mpop@mpop.dev>
6
6
  License: MIT
@@ -41,10 +41,10 @@ pip install vssh
41
41
 
42
42
  ```bash
43
43
  # Execute remote command
44
- vssh exec v1 "uptime"
44
+ vssh exec relay1 "uptime"
45
45
 
46
46
  # Transfer file at 50+ MB/s
47
- vssh put v1 ./deploy.tar.gz /opt/deploy.tar.gz
47
+ vssh put relay1 ./deploy.tar.gz /opt/deploy.tar.gz
48
48
 
49
49
  # Check connection status
50
50
  vssh status
@@ -18,10 +18,10 @@ pip install vssh
18
18
 
19
19
  ```bash
20
20
  # Execute remote command
21
- vssh exec v1 "uptime"
21
+ vssh exec relay1 "uptime"
22
22
 
23
23
  # Transfer file at 50+ MB/s
24
- vssh put v1 ./deploy.tar.gz /opt/deploy.tar.gz
24
+ vssh put relay1 ./deploy.tar.gz /opt/deploy.tar.gz
25
25
 
26
26
  # Check connection status
27
27
  vssh status
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vssh"
7
- version = "3.3.1"
7
+ version = "3.3.6"
8
8
  description = "Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -34,5 +34,8 @@ vssh-mcp = "vssh_mcp_server:main"
34
34
  Homepage = "https://github.com/meshpop/vssh"
35
35
  Repository = "https://github.com/meshpop/vssh"
36
36
 
37
+ [project.entry-points."meshpop.mcp"]
38
+ vssh = "vssh_mcp_server"
39
+
37
40
  [tool.setuptools]
38
- py-modules = ["vssh", "vssh_p2p", "vssh_p2p_fast", "vssh_quic", "vssh_mcp_server"]
41
+ py-modules = ["vssh", "vssh_p2p", "vssh_mcp_server"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vssh
3
- Version: 3.3.1
3
+ Version: 3.3.6
4
4
  Summary: Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server
5
5
  Author-email: MeshPOP <mpop@mpop.dev>
6
6
  License: MIT
@@ -41,10 +41,10 @@ pip install vssh
41
41
 
42
42
  ```bash
43
43
  # Execute remote command
44
- vssh exec v1 "uptime"
44
+ vssh exec relay1 "uptime"
45
45
 
46
46
  # Transfer file at 50+ MB/s
47
- vssh put v1 ./deploy.tar.gz /opt/deploy.tar.gz
47
+ vssh put relay1 ./deploy.tar.gz /opt/deploy.tar.gz
48
48
 
49
49
  # Check connection status
50
50
  vssh status
@@ -4,8 +4,6 @@ pyproject.toml
4
4
  vssh.py
5
5
  vssh_mcp_server.py
6
6
  vssh_p2p.py
7
- vssh_p2p_fast.py
8
- vssh_quic.py
9
7
  vssh.egg-info/PKG-INFO
10
8
  vssh.egg-info/SOURCES.txt
11
9
  vssh.egg-info/dependency_links.txt
@@ -1,3 +1,6 @@
1
1
  [console_scripts]
2
2
  vssh = vssh:main
3
3
  vssh-mcp = vssh_mcp_server:main
4
+
5
+ [meshpop.mcp]
6
+ vssh = vssh_mcp_server
@@ -1,5 +1,3 @@
1
1
  vssh
2
2
  vssh_mcp_server
3
3
  vssh_p2p
4
- vssh_p2p_fast
5
- vssh_quic
@@ -35,7 +35,14 @@ def _load_vssh_secret():
35
35
  s = sf.read_text().strip()
36
36
  if s: return s
37
37
  conf = _load_vssh_config()
38
- return conf.get('SECRET', '')
38
+ s = conf.get('SECRET', '')
39
+ if s: return s
40
+ # Auto-generate on first install
41
+ import secrets as _sec
42
+ s = _sec.token_hex(16)
43
+ VSSH_DIR_CONF.mkdir(parents=True, exist_ok=True)
44
+ sf.write_text(s)
45
+ return s
39
46
 
40
47
  VSSH_CONFIG = _load_vssh_config()
41
48
  SECRET = _load_vssh_secret()
@@ -68,7 +75,7 @@ def _load_tailscale_map() -> dict:
68
75
  if len(parts) >= 2:
69
76
  ts_map[parts[0]] = parts[1] # wire_ip tailscale_ip
70
77
  except (OSError, ValueError) as e:
71
- pass # TODO: log error
78
+ pass # e silenced
72
79
 
73
80
  return ts_map
74
81
 
@@ -103,7 +110,7 @@ def vssh_connect(host: str, timeout: float = 5.0) -> socket.socket:
103
110
  sock.connect((host, PORT))
104
111
  return sock
105
112
  except (ConnectionRefusedError, ConnectionError, OSError, socket.timeout) as wire_err:
106
- pass # TODO: log error
113
+ pass # wire_err silenced
107
114
 
108
115
  # Fallback: try Tailscale IP
109
116
  ts_ip = TAILSCALE_MAP.get(host)
@@ -380,7 +387,7 @@ def get_public_ip() -> str:
380
387
  except (urllib.error.URLError, OSError):
381
388
  continue
382
389
  except Exception as e:
383
- pass # TODO: log error
390
+ pass # e silenced
384
391
  return ''
385
392
 
386
393
  def get_network_info_local() -> dict:
@@ -435,7 +442,7 @@ def is_safe_network(host: str) -> Tuple[bool, str]:
435
442
  if 16 <= second <= 31:
436
443
  return True, "lan"
437
444
  except (ValueError, IndexError) as e:
438
- pass # TODO: log error
445
+ pass # e silenced
439
446
 
440
447
  # Everything else is public IP - UNSAFE!
441
448
  return False, "public_ip_unencrypted"
@@ -499,7 +506,7 @@ def resolve_name(host: str) -> str:
499
506
  if host in _name_cache:
500
507
  return _name_cache[host]
501
508
  break # got peers, just no match
502
- except (OSError, subprocess.SubprocessError):
509
+ except (OSError, ValueError, TimeoutError):
503
510
  continue
504
511
  return host
505
512
 
@@ -748,7 +755,7 @@ def _rpc_get_logs(payload: dict) -> dict:
748
755
  )
749
756
  return {'service': service, 'lines': r.stdout.strip().split('\n')}
750
757
  except (subprocess.SubprocessError, OSError) as e:
751
- pass # TODO: log error
758
+ pass # e silenced
752
759
 
753
760
  path = log_paths.get(service)
754
761
  if path and os.path.exists(path):
@@ -2421,11 +2428,11 @@ def pty_session(host: str, name: str = ''):
2421
2428
  sys.stdout.buffer.write(data)
2422
2429
  sys.stdout.buffer.flush()
2423
2430
  except (BlockingIOError) as e:
2424
- pass # TODO: log error
2431
+ pass # e silenced
2425
2432
  except (EOFError, ConnectionError):
2426
2433
  raise EOFError
2427
2434
  except (EOFError, KeyboardInterrupt, ConnectionError, BrokenPipeError) as e:
2428
- pass # TODO: log error
2435
+ pass # e silenced
2429
2436
  finally:
2430
2437
  # Restore terminal
2431
2438
  if old_settings:
@@ -2588,7 +2595,7 @@ class VsshSession:
2588
2595
  self.sock.sendall(f"CLOSE:{self.session_id}\n".encode())
2589
2596
  self.sock.recv(32) # CLOSE_OK
2590
2597
  except OSError as e:
2591
- pass # TODO: log error
2598
+ pass # e silenced
2592
2599
  finally:
2593
2600
  self.sock.close()
2594
2601
  self.sock = None
@@ -2961,6 +2968,57 @@ def server():
2961
2968
  result = subprocess.run(cmdline, shell=True, capture_output=True, timeout=60)
2962
2969
  conn.sendall(result.stdout + result.stderr + b'__END__')
2963
2970
 
2971
+ elif cmd == 'PTY_SESSION':
2972
+ # PTY_SESSION:auth:rows:cols → full bash PTY relay
2973
+ try:
2974
+ rows = int(parts[2]) if len(parts) > 2 else 24
2975
+ cols = int(parts[3]) if len(parts) > 3 else 80
2976
+ except (IndexError, ValueError):
2977
+ rows, cols = 24, 80
2978
+ conn.sendall(b'PTY_OK\n')
2979
+ print(f'[PTY] {addr[0]} {rows}x{cols}')
2980
+ import pty as _pty, select as _sel, fcntl as _fcntl
2981
+ import termios as _ter, struct as _stru
2982
+ mfd, sfd = _pty.openpty()
2983
+ _fcntl.ioctl(sfd, _ter.TIOCSWINSZ,
2984
+ _stru.pack('HHHH', rows, cols, 0, 0))
2985
+ proc = subprocess.Popen(
2986
+ ['/bin/bash', '-l'],
2987
+ stdin=sfd, stdout=sfd, stderr=sfd,
2988
+ preexec_fn=os.setsid, close_fds=True
2989
+ )
2990
+ os.close(sfd)
2991
+ cfd = conn.fileno()
2992
+ try:
2993
+ while proc.poll() is None:
2994
+ r, _, _ = _sel.select([cfd, mfd], [], [], 0.05)
2995
+ if cfd in r:
2996
+ data = conn.recv(4096)
2997
+ if not data:
2998
+ break
2999
+ try:
3000
+ os.write(mfd, data)
3001
+ except OSError:
3002
+ break
3003
+ if mfd in r:
3004
+ try:
3005
+ data = os.read(mfd, 4096)
3006
+ if data:
3007
+ conn.sendall(data)
3008
+ except OSError:
3009
+ break
3010
+ finally:
3011
+ try:
3012
+ proc.terminate()
3013
+ proc.wait(timeout=2)
3014
+ except Exception:
3015
+ pass
3016
+ try:
3017
+ os.close(mfd)
3018
+ except Exception:
3019
+ pass
3020
+ print(f'[PTY] {addr[0]} session ended')
3021
+
2964
3022
  elif cmd == 'INFO':
2965
3023
  conn.sendall(b'OK\n')
2966
3024
 
@@ -3213,7 +3271,7 @@ def server():
3213
3271
  try:
3214
3272
  payload = json.loads(payload_data.decode())
3215
3273
  except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
3216
- pass # TODO: log error
3274
+ pass # e silenced
3217
3275
 
3218
3276
  result = handle_rpc(method, payload, sender_ip=addr[0])
3219
3277
  conn.sendall(f'RPC_RESULT:{len(result)}\n'.encode())
@@ -3279,8 +3337,118 @@ def server():
3279
3337
  except KeyboardInterrupt:
3280
3338
  print('\nStopped')
3281
3339
 
3340
+
3341
+ def _cmd_install(args):
3342
+ """Install vssh as a system daemon (systemd on Linux, LaunchAgent on macOS)."""
3343
+ import sys as _sys, os as _os, platform as _plat, subprocess as _sp, stat as _stat
3344
+
3345
+ python_exe = _sys.executable
3346
+ vssh_py = _os.path.abspath(__file__)
3347
+ secret = _load_vssh_secret()
3348
+
3349
+ print(f"[vssh install]")
3350
+ print(f" python : {python_exe}")
3351
+ print(f" vssh.py : {vssh_py}")
3352
+ print(f" secret : {secret[:6]}...{secret[-4:]} ({'auto-generated' if not _os.environ.get('VSSH_SECRET') else 'from env'})")
3353
+
3354
+ if _plat.system() == 'Linux':
3355
+ service = f"""[Unit]
3356
+ Description=vssh daemon
3357
+ After=network.target
3358
+
3359
+ [Service]
3360
+ Type=simple
3361
+ ExecStart={python_exe} {vssh_py} server
3362
+ Environment="VSSH_SECRET={secret}"
3363
+ Restart=always
3364
+ RestartSec=5
3365
+ StandardOutput=journal
3366
+ StandardError=journal
3367
+
3368
+ [Install]
3369
+ WantedBy=multi-user.target
3370
+ """
3371
+ svc_path = '/etc/systemd/system/vssh.service'
3372
+ try:
3373
+ with open(svc_path, 'w') as f:
3374
+ f.write(service)
3375
+ print(f" Created : {svc_path}")
3376
+ _sp.run(['systemctl', 'daemon-reload'], check=True, capture_output=True)
3377
+ _sp.run(['systemctl', 'enable', 'vssh'], check=True, capture_output=True)
3378
+ _sp.run(['systemctl', 'restart', 'vssh'], check=True, capture_output=True)
3379
+ import time; time.sleep(1)
3380
+ r = _sp.run(['systemctl', 'is-active', 'vssh'], capture_output=True, text=True)
3381
+ status = r.stdout.strip()
3382
+ print(f" Service : {status}")
3383
+ if status == 'active':
3384
+ print("[vssh install] ✓ Done. vssh daemon running.")
3385
+ else:
3386
+ print("[vssh install] ✗ Service not active — check: journalctl -u vssh -n 20")
3387
+ except PermissionError:
3388
+ print("[vssh install] ✗ Need root. Run: sudo vssh install")
3389
+ except Exception as e:
3390
+ print(f"[vssh install] ✗ Error: {e}")
3391
+
3392
+ elif _plat.system() == 'Darwin':
3393
+ home = _os.path.expanduser('~')
3394
+ plist_dir = _os.path.join(home, 'Library', 'LaunchAgents')
3395
+ _os.makedirs(plist_dir, exist_ok=True)
3396
+ plist_path = _os.path.join(plist_dir, 'com.vssh.server.plist')
3397
+ plist = f"""<?xml version="1.0" encoding="UTF-8"?>
3398
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3399
+ <plist version="1.0">
3400
+ <dict>
3401
+ <key>Label</key>
3402
+ <string>com.vssh.server</string>
3403
+ <key>ProgramArguments</key>
3404
+ <array>
3405
+ <string>{python_exe}</string>
3406
+ <string>{vssh_py}</string>
3407
+ <string>server</string>
3408
+ </array>
3409
+ <key>EnvironmentVariables</key>
3410
+ <dict>
3411
+ <key>VSSH_SECRET</key>
3412
+ <string>{secret}</string>
3413
+ </dict>
3414
+ <key>RunAtLoad</key>
3415
+ <true/>
3416
+ <key>KeepAlive</key>
3417
+ <true/>
3418
+ <key>StandardOutPath</key>
3419
+ <string>/tmp/vssh.log</string>
3420
+ <key>StandardErrorPath</key>
3421
+ <string>/tmp/vssh.err</string>
3422
+ </dict>
3423
+ </plist>"""
3424
+ # Unload existing if any
3425
+ _sp.run(['launchctl', 'unload', plist_path], capture_output=True)
3426
+ with open(plist_path, 'w') as f:
3427
+ f.write(plist)
3428
+ print(f" Created : {plist_path}")
3429
+ r = _sp.run(['launchctl', 'load', plist_path], capture_output=True, text=True)
3430
+ if r.returncode == 0:
3431
+ import time; time.sleep(1)
3432
+ r2 = _sp.run(['pgrep', '-f', 'vssh.py server'], capture_output=True, text=True)
3433
+ if r2.stdout.strip():
3434
+ print("[vssh install] ✓ Done. vssh daemon running.")
3435
+ else:
3436
+ print("[vssh install] ✗ Process not found — check: cat /tmp/vssh.err")
3437
+ else:
3438
+ print(f"[vssh install] ✗ launchctl error: {r.stderr.strip()}")
3439
+ else:
3440
+ print(f"[vssh install] ✗ Unsupported OS: {_plat.system()}")
3441
+
3282
3442
  def main():
3283
3443
  args = sys.argv[1:]
3444
+ # Handle --version and --help flags
3445
+ if args and args[0] in ("--version", "-v", "-V"):
3446
+ try:
3447
+ from importlib.metadata import version as _v
3448
+ print(f"vssh v{_v('vssh')}")
3449
+ except Exception:
3450
+ print("vssh v3.3.2")
3451
+ return
3284
3452
  if not args:
3285
3453
  print('''vssh v3 - Distributed command & file transport daemon for server meshes
3286
3454
 
@@ -3469,7 +3637,7 @@ Env: VSSH_SECRET
3469
3637
  try:
3470
3638
  days = int(args[1])
3471
3639
  except (ValueError, TypeError) as e:
3472
- pass # TODO: log error
3640
+ pass # e silenced
3473
3641
  stats = get_transfer_stats(days)
3474
3642
  print(f'Transfer Statistics (last {days} days)')
3475
3643
  print(f' Total transfers: {stats["total"]}')
@@ -3586,6 +3754,8 @@ Env: VSSH_SECRET
3586
3754
  sys.exit(0 if result['fail'] == 0 else 1)
3587
3755
  elif cmd == 'server':
3588
3756
  server()
3757
+ elif cmd == 'install':
3758
+ _cmd_install(args)
3589
3759
  elif cmd == 'help' or cmd == '-h' or cmd == '--help':
3590
3760
  main_args_bak = sys.argv[1:]
3591
3761
  sys.argv = sys.argv[:1]
@@ -3607,7 +3777,7 @@ Env: VSSH_SECRET
3607
3777
  result = {'name': name, 'ip': ip, 'online': False, 'latency': 0}
3608
3778
  try:
3609
3779
  s = _sock.socket(_sock.AF_INET, _sock.SOCK_STREAM)
3610
- s.settimeout(3)
3780
+ s.settimeout(1)
3611
3781
  t0 = time.time()
3612
3782
  s.connect((ip, 48291))
3613
3783
  result['latency'] = (time.time() - t0) * 1000
@@ -3634,16 +3804,16 @@ Env: VSSH_SECRET
3634
3804
  try:
3635
3805
  result['info'] = json.loads(buf.decode())
3636
3806
  except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
3637
- pass # TODO: log error
3807
+ pass # e silenced
3638
3808
  s.close()
3639
3809
  except OSError:
3640
3810
  pass # safe to ignore
3641
3811
  return result
3642
3812
 
3643
3813
  if full_mode:
3644
- print("vssh v3.2.2 - Cluster Status (full)")
3814
+ print(f"vssh v{_get_vssh_version()} - Cluster Status (full)")
3645
3815
  else:
3646
- print("vssh v3.2.2 - Connection Status")
3816
+ print(f"vssh v{_get_vssh_version()} - Connection Status")
3647
3817
  print("=" * 70)
3648
3818
 
3649
3819
  online = 0
@@ -3723,7 +3893,7 @@ Env: VSSH_SECRET
3723
3893
  print(f" {r['name']:6s} {r['ip']:18s} ○ offline")
3724
3894
  print(f"\nTotal: {online}/{total} online")
3725
3895
  else:
3726
- # Unknown command -> try as PTY session shortcut (vssh g3 = vssh session g3)
3896
+ # Unknown command -> try as PTY session shortcut (vssh node3 = vssh session node3)
3727
3897
  host = resolve_name(cmd)
3728
3898
  rc = pty_session(host, cmd)
3729
3899
  if rc != 0: