vssh 3.3.10__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.10/vssh.egg-info → vssh-3.6.1}/PKG-INFO +1 -1
- {vssh-3.3.10 → vssh-3.6.1}/pyproject.toml +1 -1
- {vssh-3.3.10 → vssh-3.6.1/vssh.egg-info}/PKG-INFO +1 -1
- {vssh-3.3.10 → vssh-3.6.1}/vssh.py +86 -38
- {vssh-3.3.10 → vssh-3.6.1}/LICENSE +0 -0
- {vssh-3.3.10 → vssh-3.6.1}/README.md +0 -0
- {vssh-3.3.10 → vssh-3.6.1}/setup.cfg +0 -0
- {vssh-3.3.10 → vssh-3.6.1}/vssh.egg-info/SOURCES.txt +0 -0
- {vssh-3.3.10 → vssh-3.6.1}/vssh.egg-info/dependency_links.txt +0 -0
- {vssh-3.3.10 → vssh-3.6.1}/vssh.egg-info/entry_points.txt +0 -0
- {vssh-3.3.10 → vssh-3.6.1}/vssh.egg-info/top_level.txt +0 -0
- {vssh-3.3.10 → vssh-3.6.1}/vssh_mcp_server.py +0 -0
- {vssh-3.3.10 → vssh-3.6.1}/vssh_p2p.py +0 -0
|
@@ -55,7 +55,9 @@ HMAC_WINDOW = 60 # seconds - allow 60s clock skew
|
|
|
55
55
|
# Loaded from ~/.vssh/config (TAILSCALE.<wire_ip> = <ts_ip>) or ~/.vssh/tailscale_map
|
|
56
56
|
|
|
57
57
|
def _load_tailscale_map() -> dict:
|
|
58
|
-
"""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
|
+
"""
|
|
59
61
|
ts_map = {}
|
|
60
62
|
|
|
61
63
|
# 1. From config: TAILSCALE.10.99.85.143 = 100.80.191.6
|
|
@@ -75,43 +77,81 @@ def _load_tailscale_map() -> dict:
|
|
|
75
77
|
parts = line.split()
|
|
76
78
|
if len(parts) >= 2:
|
|
77
79
|
ts_map[parts[0]] = parts[1] # wire_ip tailscale_ip
|
|
78
|
-
except (OSError, ValueError)
|
|
79
|
-
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
|
|
80
113
|
|
|
81
114
|
return ts_map
|
|
82
115
|
|
|
83
116
|
TAILSCALE_MAP = _load_tailscale_map()
|
|
84
|
-
_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
|
|
85
119
|
|
|
86
120
|
def vssh_connect(host: str, timeout: float = 5.0) -> socket.socket:
|
|
87
121
|
"""Connect to vssh server with automatic Tailscale failover.
|
|
88
122
|
|
|
89
123
|
1. Try Wire VPN IP (primary)
|
|
90
|
-
2. If Wire fails → try Tailscale IP (fallback)
|
|
91
|
-
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
|
|
92
126
|
|
|
93
127
|
Returns connected socket or raises ConnectionError.
|
|
94
128
|
"""
|
|
95
|
-
# If this host
|
|
129
|
+
# If this host recently failed Wire, use cached Tailscale (but retry Wire after 60s)
|
|
96
130
|
if host in _FAILOVER_ACTIVE:
|
|
97
|
-
ts_ip = _FAILOVER_ACTIVE[host]
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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)
|
|
105
144
|
del _FAILOVER_ACTIVE[host]
|
|
106
145
|
|
|
107
146
|
# Primary: try Wire VPN IP
|
|
108
147
|
try:
|
|
109
148
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
110
|
-
sock.settimeout(timeout)
|
|
149
|
+
sock.settimeout(min(timeout, 2.0)) # 2s: enough for WireGuard handshake
|
|
111
150
|
sock.connect((host, PORT))
|
|
151
|
+
sock.settimeout(timeout) # Restore full timeout for I/O
|
|
112
152
|
return sock
|
|
113
|
-
except (ConnectionRefusedError, ConnectionError, OSError, socket.timeout)
|
|
114
|
-
pass #
|
|
153
|
+
except (ConnectionRefusedError, ConnectionError, OSError, socket.timeout):
|
|
154
|
+
pass # Wire failed, try Tailscale
|
|
115
155
|
|
|
116
156
|
# Fallback: try Tailscale IP
|
|
117
157
|
ts_ip = TAILSCALE_MAP.get(host)
|
|
@@ -122,10 +162,10 @@ def vssh_connect(host: str, timeout: float = 5.0) -> socket.socket:
|
|
|
122
162
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
123
163
|
sock.settimeout(timeout)
|
|
124
164
|
sock.connect((ts_ip, PORT))
|
|
125
|
-
_FAILOVER_ACTIVE[host] = ts_ip
|
|
165
|
+
_FAILOVER_ACTIVE[host] = (ts_ip, time.time())
|
|
126
166
|
print(f'[FAILOVER] {host} → {ts_ip} (Tailscale)')
|
|
127
167
|
return sock
|
|
128
|
-
except Exception
|
|
168
|
+
except Exception:
|
|
129
169
|
raise ConnectionError(
|
|
130
170
|
f'Both Wire ({host}) and Tailscale ({ts_ip}) failed'
|
|
131
171
|
)
|
|
@@ -492,17 +532,15 @@ def resolve_name(host: str) -> str:
|
|
|
492
532
|
for srv_ip in WIRE_SERVERS:
|
|
493
533
|
try:
|
|
494
534
|
import urllib.request
|
|
495
|
-
req = urllib.request.Request(f"http://{srv_ip}:
|
|
535
|
+
req = urllib.request.Request(f"http://{srv_ip}:8790/peers")
|
|
496
536
|
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
497
537
|
data = json.loads(resp.read())
|
|
498
538
|
peers = data.get("peers", [])
|
|
499
539
|
_name_cache.clear()
|
|
500
540
|
for p in peers:
|
|
501
541
|
vpn_ip = p.get("vpn_ip", "")
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if dn: _name_cache[dn] = vpn_ip
|
|
505
|
-
if hn: _name_cache[hn] = vpn_ip
|
|
542
|
+
nn = p.get("node_name", "")
|
|
543
|
+
if nn: _name_cache[nn] = vpn_ip
|
|
506
544
|
_name_cache_time = time.time()
|
|
507
545
|
if host in _name_cache:
|
|
508
546
|
return _name_cache[host]
|
|
@@ -3354,7 +3392,7 @@ def _cmd_install(args):
|
|
|
3354
3392
|
|
|
3355
3393
|
# --- Step 1: Upgrade pip package ---
|
|
3356
3394
|
print(f" pip : upgrading vssh...")
|
|
3357
|
-
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']
|
|
3358
3396
|
if _plat.system() == 'Linux':
|
|
3359
3397
|
pip_args.append('--break-system-packages')
|
|
3360
3398
|
r = _sp.run(pip_args, capture_output=True, text=True)
|
|
@@ -3363,21 +3401,33 @@ def _cmd_install(args):
|
|
|
3363
3401
|
else:
|
|
3364
3402
|
print(f" pip : \u2717 {r.stderr.strip()[:120]}")
|
|
3365
3403
|
|
|
3366
|
-
# --- Step 2: Find installed vssh.py (
|
|
3367
|
-
|
|
3404
|
+
# --- Step 2: Find installed vssh.py from pip (not the running script) ---
|
|
3405
|
+
installed_py = None
|
|
3368
3406
|
try:
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
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
|
|
3372
3416
|
except Exception:
|
|
3417
|
+
pass
|
|
3418
|
+
if not installed_py:
|
|
3373
3419
|
installed_py = _os.path.abspath(__file__)
|
|
3374
3420
|
|
|
3375
3421
|
# --- Step 3: Copy vssh.py to /usr/local/bin/vssh.py ---
|
|
3376
3422
|
target_py = '/usr/local/bin/vssh.py'
|
|
3377
3423
|
try:
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
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}")
|
|
3381
3431
|
except PermissionError:
|
|
3382
3432
|
print(f" warning : cannot write {target_py} (need root) — using {installed_py}")
|
|
3383
3433
|
target_py = installed_py
|
|
@@ -3831,13 +3881,11 @@ Env: VSSH_SECRET
|
|
|
3831
3881
|
_servers[k] = v
|
|
3832
3882
|
|
|
3833
3883
|
def _check_node(name, ip):
|
|
3834
|
-
"""Check a single node — connect + optional INFO query"""
|
|
3884
|
+
"""Check a single node — connect + optional INFO query (Wire VPN → Tailscale fallback)"""
|
|
3835
3885
|
result = {'name': name, 'ip': ip, 'online': False, 'latency': 0}
|
|
3836
3886
|
try:
|
|
3837
|
-
s = _sock.socket(_sock.AF_INET, _sock.SOCK_STREAM)
|
|
3838
|
-
s.settimeout(1)
|
|
3839
3887
|
t0 = time.time()
|
|
3840
|
-
s
|
|
3888
|
+
s = vssh_connect(ip, timeout=3.0)
|
|
3841
3889
|
result['latency'] = (time.time() - t0) * 1000
|
|
3842
3890
|
result['online'] = True
|
|
3843
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
|