vssh 3.6.2__tar.gz → 3.6.7__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.6.2
3
+ Version: 3.6.7
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vssh"
7
- version = "3.6.2"
7
+ version = "3.6.7"
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"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vssh
3
- Version: 3.6.2
3
+ Version: 3.6.7
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
@@ -144,14 +144,31 @@ def vssh_connect(host: str, timeout: float = 5.0) -> socket.socket:
144
144
  del _FAILOVER_ACTIVE[host]
145
145
 
146
146
  # Primary: try Wire VPN IP
147
+ # First attempt: 2s fast path
148
+ # If timeout (cold WireGuard peer re-handshake), retry once with longer timeout
149
+ _wire_refused = False
147
150
  try:
148
151
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
149
- sock.settimeout(min(timeout, 2.0)) # 2s: enough for WireGuard handshake
152
+ sock.settimeout(min(timeout, 2.0)) # 2s: fast path
150
153
  sock.connect((host, PORT))
151
- sock.settimeout(timeout) # Restore full timeout for I/O
154
+ sock.settimeout(timeout)
152
155
  return sock
153
- except (ConnectionRefusedError, ConnectionError, OSError, socket.timeout):
154
- pass # Wire failed, try Tailscale
156
+ except ConnectionRefusedError:
157
+ _wire_refused = True # Port closed no point retrying
158
+ except (ConnectionError, OSError, socket.timeout):
159
+ pass # Timeout — WireGuard may be re-handshaking, retry once
160
+
161
+ if not _wire_refused:
162
+ # Retry: wait 1s for WireGuard handshake to complete, then retry
163
+ time.sleep(1.0)
164
+ try:
165
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
166
+ sock.settimeout(min(timeout, 5.0)) # 5s: handshake should be done by now
167
+ sock.connect((host, PORT))
168
+ sock.settimeout(timeout)
169
+ return sock
170
+ except (ConnectionRefusedError, ConnectionError, OSError, socket.timeout):
171
+ pass # Wire truly failed, fall through to Tailscale
155
172
 
156
173
  # Fallback: try Tailscale IP
157
174
  ts_ip = TAILSCALE_MAP.get(host)
@@ -3866,6 +3883,65 @@ Env: VSSH_SECRET
3866
3883
  print(f' Done: {result["ok"]} ok, {result["skip"]} skip, {result["fail"]} fail')
3867
3884
 
3868
3885
  sys.exit(0 if result['fail'] == 0 else 1)
3886
+ elif cmd == 'up':
3887
+ # Start vssh daemon (systemd or direct)
3888
+ import subprocess as _sp, os as _os
3889
+ if _os.path.exists("/run/systemd/system"):
3890
+ r = _sp.run(["systemctl", "start", "vssh"], capture_output=True)
3891
+ if r.returncode == 0:
3892
+ print("vssh started")
3893
+ else:
3894
+ print(r.stderr.decode().strip() or "Failed to start vssh via systemctl")
3895
+ else:
3896
+ print("Starting vssh daemon...")
3897
+ server()
3898
+ elif cmd == 'down':
3899
+ # Stop vssh daemon
3900
+ import subprocess as _sp, os as _os
3901
+ if _os.path.exists("/run/systemd/system"):
3902
+ r = _sp.run(["systemctl", "stop", "vssh"], capture_output=True)
3903
+ if r.returncode == 0:
3904
+ print("vssh stopped")
3905
+ else:
3906
+ print(r.stderr.decode().strip() or "Failed to stop vssh via systemctl")
3907
+ else:
3908
+ import signal as _sig
3909
+ import glob as _glob
3910
+ killed = 0
3911
+ for _f in _glob.glob("/tmp/vssh_*.pid") + _glob.glob("/var/run/vssh.pid"):
3912
+ try:
3913
+ _pid = int(open(_f).read().strip())
3914
+ _os.kill(_pid, _sig.SIGTERM)
3915
+ _os.unlink(_f)
3916
+ killed += 1
3917
+ except (ValueError, OSError):
3918
+ pass
3919
+ if killed:
3920
+ print(f"vssh stopped ({killed} process(es))")
3921
+ else:
3922
+ import subprocess as _sp2
3923
+ r = _sp2.run(["pkill", "-f", "vssh.*server"], capture_output=True)
3924
+ print("vssh stopped" if r.returncode == 0 else "vssh was not running")
3925
+ elif cmd == 'restart':
3926
+ # Restart vssh daemon
3927
+ import subprocess as _sp, os as _os
3928
+ if _os.path.exists("/run/systemd/system"):
3929
+ r = _sp.run(["systemctl", "restart", "vssh"], capture_output=True)
3930
+ if r.returncode == 0:
3931
+ print("vssh restarted")
3932
+ else:
3933
+ print(r.stderr.decode().strip() or "Failed to restart vssh via systemctl")
3934
+ else:
3935
+ import signal as _sig, glob as _glob
3936
+ for _f in _glob.glob("/tmp/vssh_*.pid") + _glob.glob("/var/run/vssh.pid"):
3937
+ try:
3938
+ _pid = int(open(_f).read().strip())
3939
+ _os.kill(_pid, _sig.SIGTERM)
3940
+ _os.unlink(_f)
3941
+ except (ValueError, OSError):
3942
+ pass
3943
+ print("Restarting vssh daemon...")
3944
+ server()
3869
3945
  elif cmd == 'server':
3870
3946
  server()
3871
3947
  elif cmd == 'install':
@@ -3894,6 +3970,14 @@ Env: VSSH_SECRET
3894
3970
  break
3895
3971
  except (OSError, ValueError, TimeoutError):
3896
3972
  continue
3973
+ # Detect local node by trying to bind to each VPN IP
3974
+ def _is_local_ip(ip):
3975
+ try:
3976
+ _s = _sock.socket(_sock.AF_INET, _sock.SOCK_DGRAM)
3977
+ _s.bind((ip, 0)); _s.close(); return True
3978
+ except OSError:
3979
+ return False
3980
+ _my_vpn_ip = next((ip for ip in _servers.values() if _is_local_ip(ip)), "")
3897
3981
  # Also include any config entries not in coordinator (offline/static nodes)
3898
3982
  for k, v in VSSH_CONFIG.items():
3899
3983
  if k in ('SECRET', 'SSH_PORT', 'SCP_PORT'): continue
@@ -3902,6 +3986,8 @@ Env: VSSH_SECRET
3902
3986
 
3903
3987
  def _check_node(name, ip):
3904
3988
  """Check a single node — connect + optional INFO query (Wire VPN → Tailscale fallback)"""
3989
+ if ip == _my_vpn_ip:
3990
+ return {'name': name, 'ip': ip, 'online': True, 'latency': 0, 'local': True}
3905
3991
  result = {'name': name, 'ip': ip, 'online': False, 'latency': 0}
3906
3992
  try:
3907
3993
  t0 = time.time()
@@ -3931,7 +4017,7 @@ Env: VSSH_SECRET
3931
4017
  result['info'] = json.loads(buf.decode())
3932
4018
  except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
3933
4019
  pass # e silenced
3934
- s.close()
4020
+ s.close() # Always close — was only in full_mode, causing 13 dangling sockets on regular status
3935
4021
  except OSError:
3936
4022
  pass # safe to ignore
3937
4023
  return result
@@ -3946,7 +4032,7 @@ Env: VSSH_SECRET
3946
4032
  total = 0
3947
4033
  # Run checks in parallel for speed
3948
4034
  results = []
3949
- with concurrent.futures.ThreadPoolExecutor(max_workers=14) as pool:
4035
+ with concurrent.futures.ThreadPoolExecutor(max_workers=5) as pool: # 5 concurrent: limits WireGuard handshake burst that disrupts active sessions
3950
4036
  futures = {pool.submit(_check_node, n, ip): n for n, ip in sorted(_servers.items()) if ip}
3951
4037
  for f in concurrent.futures.as_completed(futures):
3952
4038
  results.append(f.result())
@@ -4013,6 +4099,8 @@ Env: VSSH_SECRET
4013
4099
  cores = d.get('cores', '')
4014
4100
  cores_str = f"{cores}c" if cores else ""
4015
4101
  print(f" {r['name']:6s} ● {r['latency']:4.0f}ms mem={mem_str:>12s} load={load_str:>5s} up={up_str:>7s} {cores_str}")
4102
+ elif r.get('local'):
4103
+ print(f" {r['name']:6s} {r['ip']:18s} ● online (local)")
4016
4104
  else:
4017
4105
  print(f" {r['name']:6s} {r['ip']:18s} ● online ({r['latency']:.0f}ms)")
4018
4106
  else:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes