Files
Amnezia-Web-Panel-main/managers/aivpn_manager.py
T
orohimaru2andCursor abbd160dd8 Ship AIVPN smart protocol picker and bump to v3.0.0.
Heuristic selection (stealth/balanced/speed) for invites, guest create, and user connections.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 18:11:24 +03:00

246 lines
7.0 KiB
Python

"""AIVPN — heuristic protocol picker for Amnezia Web Panel.
Picks the best installed VPN protocol on a server by strategy
(stealth / balanced / speed), optionally probing TCP reachability of ports.
This is panel-side selection (no remote AI daemon): invites, guest create,
and manual "pick now" use the same scorer.
"""
from __future__ import annotations
import logging
import socket
import time
from typing import Any, Optional
logger = logging.getLogger(__name__)
CLIENT_VPN_BASES = frozenset({
'awg', 'awg2', 'awg_legacy', 'wireguard',
'xray', 'telemt', 'hysteria', 'naiveproxy', 'mieru',
})
# Higher = preferred for that strategy (0..100 base).
STRATEGY_WEIGHTS = {
'stealth': {
'xray': 100,
'mieru': 96,
'hysteria': 92,
'naiveproxy': 88,
'telemt': 70,
'awg2': 58,
'awg': 52,
'awg_legacy': 48,
'wireguard': 40,
},
'speed': {
'awg2': 100,
'awg': 96,
'wireguard': 92,
'hysteria': 86,
'mieru': 78,
'xray': 70,
'naiveproxy': 62,
'awg_legacy': 58,
'telemt': 45,
},
'balanced': {
'hysteria': 94,
'mieru': 92,
'xray': 90,
'awg2': 88,
'awg': 82,
'naiveproxy': 78,
'wireguard': 70,
'telemt': 65,
'awg_legacy': 60,
},
}
STRATEGIES = frozenset(STRATEGY_WEIGHTS.keys())
def protocol_base(protocol: str) -> str:
return str(protocol or '').split('__', 1)[0]
def get_aivpn_settings(server: dict) -> dict:
info = server.get('server_info') or {}
raw = info.get('aivpn') if isinstance(info, dict) else None
if not isinstance(raw, dict):
raw = {}
strategy = str(raw.get('strategy') or 'balanced').lower()
if strategy not in STRATEGIES:
strategy = 'balanced'
return {
'enabled': bool(raw.get('enabled')),
'strategy': strategy,
'probe': bool(raw.get('probe', True)),
}
def set_aivpn_settings(server: dict, *, enabled: Optional[bool] = None,
strategy: Optional[str] = None,
probe: Optional[bool] = None) -> dict:
info = dict(server.get('server_info') or {})
cur = get_aivpn_settings(server)
if enabled is not None:
cur['enabled'] = bool(enabled)
if strategy is not None:
s = str(strategy).lower()
cur['strategy'] = s if s in STRATEGIES else 'balanced'
if probe is not None:
cur['probe'] = bool(probe)
info['aivpn'] = cur
server['server_info'] = info
return cur
def _tcp_rtt_ms(host: str, port: int, timeout: float = 1.2) -> Optional[float]:
if not host or not port:
return None
try:
port = int(port)
except (TypeError, ValueError):
return None
if port < 1 or port > 65535:
return None
t0 = time.perf_counter()
try:
with socket.create_connection((host, port), timeout=timeout):
return round((time.perf_counter() - t0) * 1000, 1)
except OSError:
return None
def _candidate_protocols(server: dict) -> list[str]:
protocols = server.get('protocols') or {}
out = []
for key, info in protocols.items():
if not isinstance(info, dict):
continue
if not info.get('installed'):
continue
if protocol_base(key) not in CLIENT_VPN_BASES:
continue
out.append(key)
return out
def score_protocols(
server: dict,
*,
strategy: str = 'balanced',
probe: bool = False,
live_status: Optional[dict] = None,
) -> list[dict[str, Any]]:
"""Return ranked protocol candidates with scores and reasons."""
strategy = strategy if strategy in STRATEGIES else 'balanced'
weights = STRATEGY_WEIGHTS[strategy]
host = (server.get('host') or '').strip()
protocols = server.get('protocols') or {}
live_status = live_status or {}
ranked = []
for key in _candidate_protocols(server):
base = protocol_base(key)
info = protocols.get(key) or {}
live = live_status.get(key) or {}
reasons = []
score = float(weights.get(base, 50))
reasons.append(f'base:{strategy}={int(score)}')
running = bool(live.get('container_running') or live.get('running'))
exists = bool(
live.get('container_exists')
or live.get('installed')
or info.get('installed')
)
if running:
score += 18
reasons.append('+running')
elif exists:
score -= 8
reasons.append('-not_running')
else:
score -= 40
reasons.append('-missing')
port = live.get('port') or info.get('port')
rtt = None
if probe and host and port and base != 'telemt':
# Telemt often shares 443 with other services; skip noisy probes.
rtt = _tcp_rtt_ms(host, int(port))
if rtt is None:
score -= 25
reasons.append('-port_unreachable')
elif rtt < 40:
score += 12
reasons.append(f'+rtt:{rtt}ms')
elif rtt < 120:
score += 6
reasons.append(f'+rtt:{rtt}ms')
elif rtt < 250:
reasons.append(f'rtt:{rtt}ms')
else:
score -= 8
reasons.append(f'-slow:{rtt}ms')
ranked.append({
'protocol': key,
'base': base,
'score': round(score, 1),
'port': port,
'running': running,
'rtt_ms': rtt,
'reasons': reasons,
})
ranked.sort(key=lambda x: (-x['score'], x['protocol']))
return ranked
def pick_protocol(
server: dict,
*,
strategy: Optional[str] = None,
probe: Optional[bool] = None,
live_status: Optional[dict] = None,
) -> Optional[dict[str, Any]]:
cfg = get_aivpn_settings(server)
strat = strategy or cfg['strategy']
do_probe = cfg['probe'] if probe is None else bool(probe)
ranked = score_protocols(
server,
strategy=strat,
probe=do_probe,
live_status=live_status,
)
if not ranked:
return None
best = ranked[0]
return {
'protocol': best['protocol'],
'base': best['base'],
'score': best['score'],
'strategy': strat if strat in STRATEGIES else 'balanced',
'port': best.get('port'),
'rtt_ms': best.get('rtt_ms'),
'reasons': best.get('reasons') or [],
'alternatives': ranked[1:5],
'all': ranked,
}
def resolve_provision_protocol(server: dict, requested: Optional[str] = None) -> str:
"""If requested is 'aivpn' (or empty while AIVPN enabled), pick automatically."""
req = (requested or '').strip()
cfg = get_aivpn_settings(server)
if req and protocol_base(req) != 'aivpn':
return req
if req == 'aivpn' or (not req and cfg.get('enabled')):
picked = pick_protocol(server, probe=False)
if picked and picked.get('protocol'):
return picked['protocol']
return req or 'awg'