Template
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:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user