vssh 3.3.9__tar.gz → 3.6.1__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.3.9/vssh.egg-info → vssh-3.6.1}/PKG-INFO +1 -1
- {vssh-3.3.9 → vssh-3.6.1}/pyproject.toml +1 -1
- {vssh-3.3.9 → vssh-3.6.1/vssh.egg-info}/PKG-INFO +1 -1
- {vssh-3.3.9 → vssh-3.6.1}/vssh.py +92 -41
- {vssh-3.3.9 → vssh-3.6.1}/LICENSE +0 -0
- {vssh-3.3.9 → vssh-3.6.1}/README.md +0 -0
- {vssh-3.3.9 → vssh-3.6.1}/setup.cfg +0 -0
- {vssh-3.3.9 → vssh-3.6.1}/vssh.egg-info/SOURCES.txt +0 -0
- {vssh-3.3.9 → vssh-3.6.1}/vssh.egg-info/dependency_links.txt +0 -0
- {vssh-3.3.9 → vssh-3.6.1}/vssh.egg-info/entry_points.txt +0 -0
- {vssh-3.3.9 → vssh-3.6.1}/vssh.egg-info/top_level.txt +0 -0
- {vssh-3.3.9 → vssh-3.6.1}/vssh_mcp_server.py +0 -0
- {vssh-3.3.9 → vssh-3.6.1}/vssh_p2p.py +0 -0
|
@@ -12,7 +12,6 @@ from pathlib import Path
|
|
|
12
12
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
13
13
|
from typing import Tuple, Optional
|
|
14
14
|
|
|
15
|
-
__version__ = '3.3.9'
|
|
16
15
|
|
|
17
16
|
# ========== Standalone Config (no mpop dependency) ==========
|
|
18
17
|
VSSH_DIR_CONF = Path.home() / '.vssh'
|
|
@@ -56,7 +55,9 @@ HMAC_WINDOW = 60 # seconds - allow 60s clock skew
|
|
|
56
55
|
# Loaded from ~/.vssh/config (TAILSCALE.<wire_ip> = <ts_ip>) or ~/.vssh/tailscale_map
|
|
57
56
|
|
|
58
57
|
def _load_tailscale_map() -> dict:
|
|
59
|
-
"""Load Wire VPN IP → Tailscale IP mapping
|
|
58
|
+
"""Load Wire VPN IP → Tailscale IP mapping.
|
|
59
|
+
Priority: manual config > tailscale_map file > auto-detect (tailscale + wire server)
|
|
60
|
+
"""
|
|
60
61
|
ts_map = {}
|
|
61
62
|
|
|
62
63
|
# 1. From config: TAILSCALE.10.99.85.143 = 100.80.191.6
|
|
@@ -76,43 +77,81 @@ def _load_tailscale_map() -> dict:
|
|
|
76
77
|
parts = line.split()
|
|
77
78
|
if len(parts) >= 2:
|
|
78
79
|
ts_map[parts[0]] = parts[1] # wire_ip tailscale_ip
|
|
79
|
-
except (OSError, ValueError)
|
|
80
|
-
pass
|
|
80
|
+
except (OSError, ValueError):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
# 3. Auto-detect: cross-reference `tailscale status --json` + wire /peers
|
|
84
|
+
if not ts_map:
|
|
85
|
+
try:
|
|
86
|
+
import urllib.request as _ur, subprocess as _sp2
|
|
87
|
+
ts_out = _sp2.run(['tailscale', 'status', '--json'],
|
|
88
|
+
capture_output=True, text=True, timeout=5)
|
|
89
|
+
if ts_out.returncode == 0:
|
|
90
|
+
ts_data = json.loads(ts_out.stdout)
|
|
91
|
+
ts_host = {}
|
|
92
|
+
for peer in list(ts_data.get('Peer', {}).values()) + [ts_data.get('Self', {})]:
|
|
93
|
+
if not peer:
|
|
94
|
+
continue
|
|
95
|
+
name = peer.get('HostName', '').lower().split('.')[0]
|
|
96
|
+
ips = [ip for ip in peer.get('TailscaleIPs', []) if ':' not in ip]
|
|
97
|
+
if name and ips:
|
|
98
|
+
ts_host[name] = ips[0]
|
|
99
|
+
for srv_ip in ['10.99.85.143', '158.247.247.115']:
|
|
100
|
+
try:
|
|
101
|
+
with _ur.urlopen(f'http://{srv_ip}:8790/peers', timeout=3) as r:
|
|
102
|
+
for p in json.loads(r.read()).get('peers', []):
|
|
103
|
+
name = p.get('node_name', '').lower()
|
|
104
|
+
wire_ip = p.get('vpn_ip', '')
|
|
105
|
+
ts_ip = ts_host.get(name)
|
|
106
|
+
if wire_ip and ts_ip:
|
|
107
|
+
ts_map[wire_ip] = ts_ip
|
|
108
|
+
break
|
|
109
|
+
except Exception:
|
|
110
|
+
continue
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
81
113
|
|
|
82
114
|
return ts_map
|
|
83
115
|
|
|
84
116
|
TAILSCALE_MAP = _load_tailscale_map()
|
|
85
|
-
_FAILOVER_ACTIVE = {} # host -> tailscale_ip
|
|
117
|
+
_FAILOVER_ACTIVE = {} # host -> (tailscale_ip, timestamp) — Wire retry after 60s
|
|
118
|
+
_FAILOVER_RETRY_INTERVAL = 60 # seconds before retrying Wire after failover
|
|
86
119
|
|
|
87
120
|
def vssh_connect(host: str, timeout: float = 5.0) -> socket.socket:
|
|
88
121
|
"""Connect to vssh server with automatic Tailscale failover.
|
|
89
122
|
|
|
90
123
|
1. Try Wire VPN IP (primary)
|
|
91
|
-
2. If Wire fails → try Tailscale IP (fallback)
|
|
92
|
-
3.
|
|
124
|
+
2. If Wire fails → try Tailscale IP (fallback), cache for 60s
|
|
125
|
+
3. After 60s, retry Wire — if Wire recovered, clear failover cache
|
|
93
126
|
|
|
94
127
|
Returns connected socket or raises ConnectionError.
|
|
95
128
|
"""
|
|
96
|
-
# If this host
|
|
129
|
+
# If this host recently failed Wire, use cached Tailscale (but retry Wire after 60s)
|
|
97
130
|
if host in _FAILOVER_ACTIVE:
|
|
98
|
-
ts_ip = _FAILOVER_ACTIVE[host]
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
131
|
+
ts_ip, failover_time = _FAILOVER_ACTIVE[host]
|
|
132
|
+
if time.time() - failover_time < _FAILOVER_RETRY_INTERVAL:
|
|
133
|
+
# Still within retry window — use Tailscale directly
|
|
134
|
+
try:
|
|
135
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
136
|
+
sock.settimeout(timeout)
|
|
137
|
+
sock.connect((ts_ip, PORT))
|
|
138
|
+
return sock
|
|
139
|
+
except (OSError, socket.error):
|
|
140
|
+
# Tailscale also failed — clear cache, fall through to Wire retry
|
|
141
|
+
del _FAILOVER_ACTIVE[host]
|
|
142
|
+
else:
|
|
143
|
+
# 60s elapsed — try Wire again first (Wire may have recovered)
|
|
106
144
|
del _FAILOVER_ACTIVE[host]
|
|
107
145
|
|
|
108
146
|
# Primary: try Wire VPN IP
|
|
109
147
|
try:
|
|
110
148
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
111
|
-
sock.settimeout(timeout)
|
|
149
|
+
sock.settimeout(min(timeout, 2.0)) # 2s: enough for WireGuard handshake
|
|
112
150
|
sock.connect((host, PORT))
|
|
151
|
+
sock.settimeout(timeout) # Restore full timeout for I/O
|
|
113
152
|
return sock
|
|
114
|
-
except (ConnectionRefusedError, ConnectionError, OSError, socket.timeout)
|
|
115
|
-
pass #
|
|
153
|
+
except (ConnectionRefusedError, ConnectionError, OSError, socket.timeout):
|
|
154
|
+
pass # Wire failed, try Tailscale
|
|
116
155
|
|
|
117
156
|
# Fallback: try Tailscale IP
|
|
118
157
|
ts_ip = TAILSCALE_MAP.get(host)
|
|
@@ -123,10 +162,10 @@ def vssh_connect(host: str, timeout: float = 5.0) -> socket.socket:
|
|
|
123
162
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
124
163
|
sock.settimeout(timeout)
|
|
125
164
|
sock.connect((ts_ip, PORT))
|
|
126
|
-
_FAILOVER_ACTIVE[host] = ts_ip
|
|
165
|
+
_FAILOVER_ACTIVE[host] = (ts_ip, time.time())
|
|
127
166
|
print(f'[FAILOVER] {host} → {ts_ip} (Tailscale)')
|
|
128
167
|
return sock
|
|
129
|
-
except Exception
|
|
168
|
+
except Exception:
|
|
130
169
|
raise ConnectionError(
|
|
131
170
|
f'Both Wire ({host}) and Tailscale ({ts_ip}) failed'
|
|
132
171
|
)
|
|
@@ -493,17 +532,15 @@ def resolve_name(host: str) -> str:
|
|
|
493
532
|
for srv_ip in WIRE_SERVERS:
|
|
494
533
|
try:
|
|
495
534
|
import urllib.request
|
|
496
|
-
req = urllib.request.Request(f"http://{srv_ip}:
|
|
535
|
+
req = urllib.request.Request(f"http://{srv_ip}:8790/peers")
|
|
497
536
|
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
498
537
|
data = json.loads(resp.read())
|
|
499
538
|
peers = data.get("peers", [])
|
|
500
539
|
_name_cache.clear()
|
|
501
540
|
for p in peers:
|
|
502
541
|
vpn_ip = p.get("vpn_ip", "")
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if dn: _name_cache[dn] = vpn_ip
|
|
506
|
-
if hn: _name_cache[hn] = vpn_ip
|
|
542
|
+
nn = p.get("node_name", "")
|
|
543
|
+
if nn: _name_cache[nn] = vpn_ip
|
|
507
544
|
_name_cache_time = time.time()
|
|
508
545
|
if host in _name_cache:
|
|
509
546
|
return _name_cache[host]
|
|
@@ -3355,7 +3392,7 @@ def _cmd_install(args):
|
|
|
3355
3392
|
|
|
3356
3393
|
# --- Step 1: Upgrade pip package ---
|
|
3357
3394
|
print(f" pip : upgrading vssh...")
|
|
3358
|
-
pip_args = [python_exe, '-m', 'pip', 'install', 'vssh', '--upgrade', '-q']
|
|
3395
|
+
pip_args = [python_exe, '-m', 'pip', 'install', 'git+https://github.com/meshpop/vssh.git', '--upgrade', '-q']
|
|
3359
3396
|
if _plat.system() == 'Linux':
|
|
3360
3397
|
pip_args.append('--break-system-packages')
|
|
3361
3398
|
r = _sp.run(pip_args, capture_output=True, text=True)
|
|
@@ -3364,21 +3401,33 @@ def _cmd_install(args):
|
|
|
3364
3401
|
else:
|
|
3365
3402
|
print(f" pip : \u2717 {r.stderr.strip()[:120]}")
|
|
3366
3403
|
|
|
3367
|
-
# --- Step 2: Find installed vssh.py (
|
|
3368
|
-
|
|
3404
|
+
# --- Step 2: Find installed vssh.py from pip (not the running script) ---
|
|
3405
|
+
installed_py = None
|
|
3369
3406
|
try:
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3407
|
+
r2 = _sp.run([python_exe, '-m', 'pip', 'show', 'vssh'],
|
|
3408
|
+
capture_output=True, text=True)
|
|
3409
|
+
for _line in r2.stdout.splitlines():
|
|
3410
|
+
if _line.startswith('Location:'):
|
|
3411
|
+
_loc = _line.split(':', 1)[1].strip()
|
|
3412
|
+
_cand = _os.path.join(_loc, 'vssh.py')
|
|
3413
|
+
if _os.path.isfile(_cand):
|
|
3414
|
+
installed_py = _cand
|
|
3415
|
+
break
|
|
3373
3416
|
except Exception:
|
|
3417
|
+
pass
|
|
3418
|
+
if not installed_py:
|
|
3374
3419
|
installed_py = _os.path.abspath(__file__)
|
|
3375
3420
|
|
|
3376
3421
|
# --- Step 3: Copy vssh.py to /usr/local/bin/vssh.py ---
|
|
3377
3422
|
target_py = '/usr/local/bin/vssh.py'
|
|
3378
3423
|
try:
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3424
|
+
import os.path as _osp
|
|
3425
|
+
if _osp.exists(target_py) and _osp.samefile(installed_py, target_py):
|
|
3426
|
+
print(f" skipped : already up to date ({target_py})")
|
|
3427
|
+
else:
|
|
3428
|
+
_sh.copy2(installed_py, target_py)
|
|
3429
|
+
_os.chmod(target_py, 0o755)
|
|
3430
|
+
print(f" copied : {installed_py} \u2192 {target_py}")
|
|
3382
3431
|
except PermissionError:
|
|
3383
3432
|
print(f" warning : cannot write {target_py} (need root) — using {installed_py}")
|
|
3384
3433
|
target_py = installed_py
|
|
@@ -3490,7 +3539,11 @@ def _cmd_install(args):
|
|
|
3490
3539
|
|
|
3491
3540
|
|
|
3492
3541
|
def _get_vssh_version():
|
|
3493
|
-
|
|
3542
|
+
try:
|
|
3543
|
+
from importlib.metadata import version as _v
|
|
3544
|
+
return _v('vssh')
|
|
3545
|
+
except Exception:
|
|
3546
|
+
return '?'
|
|
3494
3547
|
|
|
3495
3548
|
|
|
3496
3549
|
|
|
@@ -3502,7 +3555,7 @@ def main():
|
|
|
3502
3555
|
from importlib.metadata import version as _v
|
|
3503
3556
|
print(f"vssh v{_v('vssh')}")
|
|
3504
3557
|
except Exception:
|
|
3505
|
-
print(f"vssh v{
|
|
3558
|
+
print(f"vssh v{_get_vssh_version()}")
|
|
3506
3559
|
return
|
|
3507
3560
|
if not args:
|
|
3508
3561
|
print('''vssh v3 - Distributed command & file transport daemon for server meshes
|
|
@@ -3828,13 +3881,11 @@ Env: VSSH_SECRET
|
|
|
3828
3881
|
_servers[k] = v
|
|
3829
3882
|
|
|
3830
3883
|
def _check_node(name, ip):
|
|
3831
|
-
"""Check a single node — connect + optional INFO query"""
|
|
3884
|
+
"""Check a single node — connect + optional INFO query (Wire VPN → Tailscale fallback)"""
|
|
3832
3885
|
result = {'name': name, 'ip': ip, 'online': False, 'latency': 0}
|
|
3833
3886
|
try:
|
|
3834
|
-
s = _sock.socket(_sock.AF_INET, _sock.SOCK_STREAM)
|
|
3835
|
-
s.settimeout(1)
|
|
3836
3887
|
t0 = time.time()
|
|
3837
|
-
s
|
|
3888
|
+
s = vssh_connect(ip, timeout=3.0)
|
|
3838
3889
|
result['latency'] = (time.time() - t0) * 1000
|
|
3839
3890
|
result['online'] = True
|
|
3840
3891
|
|
|
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
|