vssh 3.6.1__tar.gz → 3.6.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.
- {vssh-3.6.1/vssh.egg-info → vssh-3.6.6}/PKG-INFO +1 -1
- {vssh-3.6.1 → vssh-3.6.6}/pyproject.toml +1 -1
- {vssh-3.6.1 → vssh-3.6.6/vssh.egg-info}/PKG-INFO +1 -1
- {vssh-3.6.1 → vssh-3.6.6}/vssh.py +123 -15
- {vssh-3.6.1 → vssh-3.6.6}/LICENSE +0 -0
- {vssh-3.6.1 → vssh-3.6.6}/README.md +0 -0
- {vssh-3.6.1 → vssh-3.6.6}/setup.cfg +0 -0
- {vssh-3.6.1 → vssh-3.6.6}/vssh.egg-info/SOURCES.txt +0 -0
- {vssh-3.6.1 → vssh-3.6.6}/vssh.egg-info/dependency_links.txt +0 -0
- {vssh-3.6.1 → vssh-3.6.6}/vssh.egg-info/entry_points.txt +0 -0
- {vssh-3.6.1 → vssh-3.6.6}/vssh.egg-info/top_level.txt +0 -0
- {vssh-3.6.1 → vssh-3.6.6}/vssh_mcp_server.py +0 -0
- {vssh-3.6.1 → vssh-3.6.6}/vssh_p2p.py +0 -0
|
@@ -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:
|
|
152
|
+
sock.settimeout(min(timeout, 2.0)) # 2s: fast path
|
|
150
153
|
sock.connect((host, PORT))
|
|
151
|
-
sock.settimeout(timeout)
|
|
154
|
+
sock.settimeout(timeout)
|
|
152
155
|
return sock
|
|
153
|
-
except
|
|
154
|
-
|
|
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)
|
|
@@ -515,23 +532,26 @@ _name_cache = {} # name -> vpn_ip
|
|
|
515
532
|
_name_cache_time = 0
|
|
516
533
|
|
|
517
534
|
def resolve_name(host: str) -> str:
|
|
518
|
-
"""Resolve server name to VPN IP via Wire VPN /peers API (
|
|
535
|
+
"""Resolve server name to VPN IP via Wire VPN /peers API (coordinator-first)
|
|
536
|
+
|
|
537
|
+
Priority:
|
|
538
|
+
1. Fresh in-memory cache (< 60s) — avoids repeated network calls
|
|
539
|
+
2. Wire coordinator /peers — source of truth (IPs change when nodes move)
|
|
540
|
+
3. ~/.vssh/config static mapping — fallback when coordinator unreachable
|
|
541
|
+
"""
|
|
519
542
|
global _name_cache, _name_cache_time
|
|
520
543
|
# Strip user@ prefix
|
|
521
544
|
if '@' in host:
|
|
522
545
|
host = host.split('@', 1)[1]
|
|
523
546
|
if not host or host[0].isdigit():
|
|
524
547
|
return host
|
|
525
|
-
# 1.
|
|
526
|
-
if host in VSSH_CONFIG:
|
|
527
|
-
return VSSH_CONFIG[host]
|
|
528
|
-
# Use cache if fresh (< 60s)
|
|
548
|
+
# 1. Use fresh cache — coordinator already queried recently
|
|
529
549
|
if host in _name_cache and time.time() - _name_cache_time < 60:
|
|
530
550
|
return _name_cache[host]
|
|
531
|
-
# Query Wire
|
|
551
|
+
# 2. Query Wire coordinator (source of truth — IPs change when nodes move)
|
|
552
|
+
import urllib.request
|
|
532
553
|
for srv_ip in WIRE_SERVERS:
|
|
533
554
|
try:
|
|
534
|
-
import urllib.request
|
|
535
555
|
req = urllib.request.Request(f"http://{srv_ip}:8790/peers")
|
|
536
556
|
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
537
557
|
data = json.loads(resp.read())
|
|
@@ -544,9 +564,12 @@ def resolve_name(host: str) -> str:
|
|
|
544
564
|
_name_cache_time = time.time()
|
|
545
565
|
if host in _name_cache:
|
|
546
566
|
return _name_cache[host]
|
|
547
|
-
break # got peers, just no match
|
|
567
|
+
break # got peers from coordinator, just no match for this host
|
|
548
568
|
except (OSError, ValueError, TimeoutError):
|
|
549
569
|
continue
|
|
570
|
+
# 3. Fall back to static config (useful when all coordinators unreachable)
|
|
571
|
+
if host in VSSH_CONFIG:
|
|
572
|
+
return VSSH_CONFIG[host]
|
|
550
573
|
return host
|
|
551
574
|
|
|
552
575
|
|
|
@@ -3860,6 +3883,65 @@ Env: VSSH_SECRET
|
|
|
3860
3883
|
print(f' Done: {result["ok"]} ok, {result["skip"]} skip, {result["fail"]} fail')
|
|
3861
3884
|
|
|
3862
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()
|
|
3863
3945
|
elif cmd == 'server':
|
|
3864
3946
|
server()
|
|
3865
3947
|
elif cmd == 'install':
|
|
@@ -3873,15 +3955,39 @@ Env: VSSH_SECRET
|
|
|
3873
3955
|
import socket as _sock
|
|
3874
3956
|
import concurrent.futures
|
|
3875
3957
|
full_mode = '--full' in args or '-f' in args
|
|
3876
|
-
#
|
|
3958
|
+
# Build server list: coordinator first (source of truth), config as supplement
|
|
3877
3959
|
_servers = {}
|
|
3960
|
+
import urllib.request as _ureq
|
|
3961
|
+
for _srv_ip in WIRE_SERVERS:
|
|
3962
|
+
try:
|
|
3963
|
+
with _ureq.urlopen(f"http://{_srv_ip}:8790/peers", timeout=3) as _r:
|
|
3964
|
+
_peers = json.loads(_r.read()).get("peers", [])
|
|
3965
|
+
for _p in _peers:
|
|
3966
|
+
_nn = _p.get("node_name", "")
|
|
3967
|
+
_vip = _p.get("vpn_ip", "")
|
|
3968
|
+
if _nn and _vip:
|
|
3969
|
+
_servers[_nn] = _vip
|
|
3970
|
+
break
|
|
3971
|
+
except (OSError, ValueError, TimeoutError):
|
|
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)), "")
|
|
3981
|
+
# Also include any config entries not in coordinator (offline/static nodes)
|
|
3878
3982
|
for k, v in VSSH_CONFIG.items():
|
|
3879
|
-
if k in ('SECRET','SSH_PORT','SCP_PORT'): continue
|
|
3880
|
-
if v and v[0].isdigit():
|
|
3983
|
+
if k in ('SECRET', 'SSH_PORT', 'SCP_PORT'): continue
|
|
3984
|
+
if v and v[0].isdigit() and k not in _servers:
|
|
3881
3985
|
_servers[k] = v
|
|
3882
3986
|
|
|
3883
3987
|
def _check_node(name, ip):
|
|
3884
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}
|
|
3885
3991
|
result = {'name': name, 'ip': ip, 'online': False, 'latency': 0}
|
|
3886
3992
|
try:
|
|
3887
3993
|
t0 = time.time()
|
|
@@ -3993,6 +4099,8 @@ Env: VSSH_SECRET
|
|
|
3993
4099
|
cores = d.get('cores', '')
|
|
3994
4100
|
cores_str = f"{cores}c" if cores else ""
|
|
3995
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)")
|
|
3996
4104
|
else:
|
|
3997
4105
|
print(f" {r['name']:6s} {r['ip']:18s} ● online ({r['latency']:.0f}ms)")
|
|
3998
4106
|
else:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|