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>
This commit is contained in:
orohimaru2
2026-08-09 18:11:24 +03:00
co-authored by Cursor
parent 01196c066a
commit abbd160dd8
12 changed files with 587 additions and 52 deletions
+125 -25
View File
@@ -103,7 +103,7 @@ else:
application_path = os.path.dirname(__file__)
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
CURRENT_VERSION = "v2.7.2"
CURRENT_VERSION = "v3.0.0"
RELEASES_REPO_URL = repo_url()
RELEASES_API_LATEST = api_latest_url()
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
@@ -266,6 +266,7 @@ def tpl(request, template, **kwargs):
'translations_json': json.dumps(TRANSLATIONS.get(lang, TRANSLATIONS.get('en', {}))),
# Keep for legacy JS; prefer translations_json on new pages
'all_translations_json': json.dumps(TRANSLATIONS),
'releases_repo_url': RELEASES_REPO_URL,
}
ctx.update(kwargs)
return templates.TemplateResponse(template, ctx)
@@ -4273,6 +4274,74 @@ async def api_server_migrate_import(
return JSONResponse({'error': str(e)}, status_code=400)
class AivpnSettingsRequest(BaseModel):
enabled: Optional[bool] = None
strategy: Optional[str] = None # stealth | balanced | speed
probe: Optional[bool] = None
@app.get('/api/servers/{server_id}/aivpn', tags=["Servers"])
async def api_aivpn_get(request: Request, server_id: int, probe: bool = False):
"""AIVPN settings + ranked protocol recommendation for this server."""
if not _check_admin(request):
return JSONResponse({'error': 'Forbidden'}, status_code=403)
from managers.aivpn_manager import get_aivpn_settings, pick_protocol
data = await load_data_async()
if server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
server = data['servers'][server_id]
settings = get_aivpn_settings(server)
live_status = {}
try:
# Reuse last known protocol metadata; optional live SSH probe of ports only.
for key, info in (server.get('protocols') or {}).items():
if isinstance(info, dict):
live_status[key] = {
'installed': bool(info.get('installed')),
'port': info.get('port'),
'container_running': bool(info.get('running')),
'container_exists': bool(info.get('installed')),
}
except Exception:
pass
do_probe = bool(probe) if probe else bool(settings.get('probe'))
picked = await asyncio.to_thread(
pick_protocol,
server,
strategy=settings.get('strategy'),
probe=do_probe,
live_status=live_status,
)
return {
'settings': settings,
'recommendation': picked,
'connect_domain': get_server_connect_host(server, (picked or {}).get('protocol')),
}
@app.post('/api/servers/{server_id}/aivpn', tags=["Servers"])
async def api_aivpn_save(request: Request, server_id: int, req: AivpnSettingsRequest):
if not _check_admin(request):
return JSONResponse({'error': 'Forbidden'}, status_code=403)
from managers.aivpn_manager import set_aivpn_settings, pick_protocol, get_aivpn_settings
async with DATA_LOCK:
data = load_data()
if server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
server = data['servers'][server_id]
settings = set_aivpn_settings(
server,
enabled=req.enabled,
strategy=req.strategy,
probe=req.probe,
)
save_data(data)
picked = pick_protocol(server, probe=False)
return {'status': 'success', 'settings': settings, 'recommendation': picked}
@app.post('/api/servers/{server_id}/backups/export-clients', tags=["Protocols"])
async def api_protocol_export_clients(request: Request, server_id: int, req: ProtocolRequest):
"""Download a ZIP with all reconstructable WireGuard/AWG client .conf files."""
@@ -4812,7 +4881,13 @@ async def api_add_connection(request: Request, server_id: int, req: AddConnectio
if server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
if protocol_base(req.protocol) == 'xui':
protocol = req.protocol
server = data['servers'][server_id]
if protocol_base(protocol) == 'aivpn':
from managers.aivpn_manager import resolve_provision_protocol
protocol = resolve_provision_protocol(server, 'aivpn')
if protocol_base(protocol) == 'xui':
from managers.xui_api import xui_create_vless_config
from managers.xui_servers import get_xui_server, ensure_xui_servers
ensure_xui_servers(data.setdefault('settings', {}))
@@ -4846,16 +4921,15 @@ async def api_add_connection(request: Request, server_id: int, req: AddConnectio
save_data(data)
return result
server = data['servers'][server_id]
proto_info = server.get('protocols', {}).get(req.protocol, {})
proto_info = server.get('protocols', {}).get(protocol, {})
port = proto_info.get('port', '55424')
ssh = get_ssh(server)
ssh.connect()
manager = get_protocol_manager(ssh, req.protocol)
manager = get_protocol_manager(ssh, protocol)
if protocol_base(req.protocol) == 'telemt':
if protocol_base(protocol) == 'telemt':
result = manager.add_client(
req.protocol, req.name, get_server_connect_host(server, req.protocol), port,
protocol, req.name, get_server_connect_host(server, protocol), port,
telemt_quota=req.telemt_quota,
telemt_max_ips=req.telemt_max_ips,
telemt_expiry=req.telemt_expiry,
@@ -4863,14 +4937,15 @@ async def api_add_connection(request: Request, server_id: int, req: AddConnectio
user_ad_tag=req.telemt_ad_tag,
max_tcp_conns=req.telemt_max_conns
)
elif protocol_base(req.protocol) == 'wireguard':
result = manager.add_client(req.name, get_server_connect_host(server, req.protocol))
elif protocol_base(protocol) == 'wireguard':
result = manager.add_client(req.name, get_server_connect_host(server, protocol))
else:
result = manager.add_client(req.protocol, req.name, get_server_connect_host(server, req.protocol), port)
result = manager.add_client(protocol, req.name, get_server_connect_host(server, protocol), port)
ssh.disconnect()
if result.get('config'):
result['vpn_link'] = generate_vpn_link(result['config'])
result['protocol'] = protocol
# Link connection to user if specified
if req.user_id and result.get('client_id'):
@@ -4878,7 +4953,7 @@ async def api_add_connection(request: Request, server_id: int, req: AddConnectio
'id': str(uuid.uuid4()),
'user_id': req.user_id,
'server_id': server_id,
'protocol': req.protocol,
'protocol': protocol,
'client_id': result['client_id'],
'name': req.name,
'created_at': datetime.now().isoformat(),
@@ -5316,13 +5391,20 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
user = next((u for u in data['users'] if u['id'] == user_id), None)
if not user:
return JSONResponse({'error': 'User not found'}, status_code=404)
if protocol_base(req.protocol) not in CLIENT_VPN_BASES:
protocol = req.protocol
if protocol_base(protocol) == 'aivpn':
from managers.aivpn_manager import resolve_provision_protocol
if req.server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
protocol = resolve_provision_protocol(data['servers'][req.server_id], 'aivpn')
if protocol_base(protocol) not in CLIENT_VPN_BASES:
return JSONResponse(
{'error': f'Protocol "{req.protocol}" does not support user connections'},
{'error': f'Protocol "{protocol}" does not support user connections'},
status_code=400,
)
if protocol_base(req.protocol) == 'xui':
if protocol_base(protocol) == 'xui':
from managers.xui_servers import get_xui_server, ensure_xui_servers
ensure_xui_servers(data.setdefault('settings', {}))
panel = get_xui_server(data.get('settings') or {}, req.xui_panel_id)
@@ -5378,23 +5460,23 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
if req.server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
server = data['servers'][req.server_id]
proto_info = server.get('protocols', {}).get(req.protocol, {})
proto_info = server.get('protocols', {}).get(protocol, {})
port = proto_info.get('port', '55424')
ssh = get_ssh(server)
await asyncio.to_thread(ssh.connect)
try:
manager = get_protocol_manager(ssh, req.protocol)
manager = get_protocol_manager(ssh, protocol)
if req.client_id:
# Link existing client
config = await asyncio.to_thread(
_manager_call, manager, 'get_client_config',
req.protocol, req.client_id, get_server_connect_host(server, req.protocol), port,
protocol, req.client_id, get_server_connect_host(server, protocol), port,
)
result = {'client_id': req.client_id, 'config': config}
elif protocol_base(req.protocol) == 'telemt':
elif protocol_base(protocol) == 'telemt':
result = await asyncio.to_thread(
manager.add_client, req.protocol, req.name, get_server_connect_host(server, req.protocol), port,
manager.add_client, protocol, req.name, get_server_connect_host(server, protocol), port,
telemt_quota=req.telemt_quota,
telemt_max_ips=req.telemt_max_ips,
telemt_expiry=req.telemt_expiry,
@@ -5402,12 +5484,12 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
user_ad_tag=req.telemt_ad_tag,
max_tcp_conns=req.telemt_max_conns,
)
elif protocol_base(req.protocol) == 'wireguard':
result = await asyncio.to_thread(manager.add_client, req.name, get_server_connect_host(server, req.protocol))
elif protocol_base(protocol) == 'wireguard':
result = await asyncio.to_thread(manager.add_client, req.name, get_server_connect_host(server, protocol))
else:
result = await asyncio.to_thread(
_manager_call, manager, 'add_client',
req.protocol, req.name, get_server_connect_host(server, req.protocol), port,
protocol, req.name, get_server_connect_host(server, protocol), port,
)
finally:
await asyncio.to_thread(ssh.disconnect)
@@ -5417,7 +5499,7 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
'id': str(uuid.uuid4()),
'user_id': user_id,
'server_id': req.server_id,
'protocol': req.protocol,
'protocol': protocol,
'client_id': result['client_id'],
'name': req.name,
'created_at': datetime.now().isoformat(),
@@ -5788,6 +5870,13 @@ async def api_guest_create(token: str, req: GuestCreateRequest, request: Request
if user_is_expired(holder):
return JSONResponse({'error': 'Subscription expired'}, status_code=403)
if protocol_base(protocol) == 'aivpn':
from managers.aivpn_manager import resolve_provision_protocol
sid = int(guest.get('create_server_id') or 0)
if sid >= len(data['servers']):
return JSONResponse({'error': 'Guest server not found'}, status_code=400)
protocol = resolve_provision_protocol(data['servers'][sid], 'aivpn')
if protocol_base(protocol) == 'xui':
from managers.xui_api import xui_create_vless_config
from managers.xui_servers import get_xui_server, ensure_xui_servers
@@ -5939,6 +6028,12 @@ async def _create_config_for_protocol(
) -> dict:
"""Create VPN client; returns {client_id, config, subscription_url, protocol, server_id}."""
protocol = protocol or 'xui'
if protocol_base(protocol) == 'aivpn':
from managers.aivpn_manager import resolve_provision_protocol
sid = int(server_id or 0)
if sid >= len(data.get('servers') or []):
raise RuntimeError('Server not found')
protocol = resolve_provision_protocol(data['servers'][sid], 'aivpn')
if protocol_base(protocol) == 'xui':
from managers.xui_api import xui_create_vless_config
from managers.xui_servers import get_xui_server, ensure_xui_servers
@@ -6256,11 +6351,16 @@ async def api_invite_create_config(token: str, req: InviteRedeemRequest, request
name = f"{name}_{secrets.token_hex(3)}"
try:
data = load_data()
protocol = link.get('protocol') or 'xui'
sid = int(link.get('server_id') or 0)
if protocol_base(protocol) == 'aivpn' and sid < len(data.get('servers') or []):
from managers.aivpn_manager import resolve_provision_protocol
protocol = resolve_provision_protocol(data['servers'][sid], 'aivpn')
created = await _create_config_for_protocol(
data,
protocol=link.get('protocol') or 'xui',
protocol=protocol,
name=name,
server_id=int(link.get('server_id') or 0),
server_id=sid,
xui_inbound_id=int(link.get('xui_inbound_id') or 0) or None,
xui_panel_id=link.get('xui_panel_id') or None,
duration_days=int(link.get('duration_days') or 0),