Fix linking existing VPN clients (WireGuard/service protocols and API errors).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-26 00:31:57 +03:00
co-authored by Cursor
parent 69886f130c
commit e612a7bc34
8 changed files with 109 additions and 46 deletions
+69 -30
View File
@@ -956,10 +956,17 @@ def _manager_call(manager, method, protocol, *args, **kwargs):
"""Unified call: WireGuard manager methods don't take protocol_type argument."""
fn = getattr(manager, method)
if isinstance(manager, WireGuardManager):
# WireGuard signatures omit protocol and often omit port.
if method in ('get_client_config', 'add_client') and len(args) >= 2:
return fn(args[0], args[1])
return fn(*args, **kwargs)
return fn(protocol, *args, **kwargs)
# Protocols that own VPN clients (can list/link connections)
CLIENT_VPN_BASES = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard'}
def generate_vpn_link(config_text):
b64 = base64.b64encode(config_text.strip().encode('utf-8')).decode('utf-8')
return f"vpn://{b64}"
@@ -3616,22 +3623,27 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
return JSONResponse({'error': 'User not found'}, status_code=404)
if req.server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
if protocol_base(req.protocol) not in CLIENT_VPN_BASES:
return JSONResponse(
{'error': f'Protocol "{req.protocol}" does not support user connections'},
status_code=400,
)
server = data['servers'][req.server_id]
proto_info = server.get('protocols', {}).get(req.protocol, {})
port = proto_info.get('port', '55424')
ssh = get_ssh(server)
await asyncio.to_thread(ssh.connect)
manager = get_protocol_manager(ssh, req.protocol)
try:
manager = get_protocol_manager(ssh, req.protocol)
if req.client_id:
# Use existing client
target_client_id = req.client_id
# Retrieve config for existing client
config = await asyncio.to_thread(manager.get_client_config, req.protocol, req.client_id, server['host'], port)
result = {'client_id': target_client_id, 'config': config}
else:
# Create new client
if protocol_base(req.protocol) == 'telemt':
if req.client_id:
# Link existing client
config = await asyncio.to_thread(
_manager_call, manager, 'get_client_config',
req.protocol, req.client_id, server['host'], port,
)
result = {'client_id': req.client_id, 'config': config}
elif protocol_base(req.protocol) == 'telemt':
result = await asyncio.to_thread(
manager.add_client, req.protocol, req.name, server['host'], port,
telemt_quota=req.telemt_quota,
@@ -3639,12 +3651,17 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
telemt_expiry=req.telemt_expiry,
secret=req.telemt_secret,
user_ad_tag=req.telemt_ad_tag,
max_tcp_conns=req.telemt_max_conns
max_tcp_conns=req.telemt_max_conns,
)
elif protocol_base(req.protocol) == 'wireguard':
result = await asyncio.to_thread(manager.add_client, req.name, server['host'])
else:
result = await asyncio.to_thread(manager.add_client, req.protocol, req.name, server['host'], port)
await asyncio.to_thread(ssh.disconnect)
result = await asyncio.to_thread(
_manager_call, manager, 'add_client',
req.protocol, req.name, server['host'], port,
)
finally:
await asyncio.to_thread(ssh.disconnect)
if result.get('client_id'):
conn = {
@@ -3656,9 +3673,10 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
'name': req.name,
'created_at': datetime.now().isoformat(),
}
data = load_data()
data['user_connections'].append(conn)
save_data(data)
async with DATA_LOCK:
data = load_data()
data['user_connections'].append(conn)
save_data(data)
resp = {'status': 'success'}
if result.get('config'):
@@ -3667,7 +3685,7 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
return resp
except Exception as e:
logger.exception("Error adding user connection")
return JSONResponse({'error': str(e)}, status_code=500)
return JSONResponse({'error': str(e) or e.__class__.__name__}, status_code=500)
@app.get('/api/users/{user_id}/connections', tags=["Users"])
@@ -4092,31 +4110,52 @@ async def api_get_server_clients(request: Request, server_id: int, protocol: str
if not _check_admin(request):
return JSONResponse({'error': 'Forbidden'}, status_code=403)
try:
if not protocol or protocol_base(protocol) not in CLIENT_VPN_BASES:
return JSONResponse(
{'error': f'Protocol "{protocol}" does not support client listing'},
status_code=400,
)
data = load_data()
if server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
server = data['servers'][server_id]
ssh = get_ssh(server)
ssh.connect()
manager = get_protocol_manager(ssh, protocol)
clients = manager.get_clients(protocol)
ssh.disconnect()
await asyncio.to_thread(ssh.connect)
try:
manager = get_protocol_manager(ssh, protocol)
if not hasattr(manager, 'get_clients'):
return JSONResponse(
{'error': f'Manager for "{protocol}" has no get_clients'},
status_code=400,
)
clients = await asyncio.to_thread(_manager_call, manager, 'get_clients', protocol)
finally:
await asyncio.to_thread(ssh.disconnect)
# Filter: only show clients that are not assigned to anyone in the panel
assigned_ids = {c['client_id'] for c in data.get('user_connections', []) if c['server_id'] == server_id and c['protocol'] == protocol}
assigned_ids = {
c['client_id']
for c in data.get('user_connections', [])
if c.get('server_id') == server_id and c.get('protocol') == protocol
}
filtered = []
for c in clients:
if c['clientId'] not in assigned_ids:
filtered.append({
'id': c['clientId'],
'name': c.get('userData', {}).get('clientName', 'Unnamed')
})
for c in clients or []:
cid = c.get('clientId') or c.get('client_id') or c.get('id')
if not cid or cid in assigned_ids:
continue
filtered.append({
'id': cid,
'name': (c.get('userData') or {}).get('clientName')
or c.get('name')
or c.get('email')
or 'Unnamed',
})
return {'clients': filtered}
except Exception as e:
logger.exception("Error getting server clients")
return JSONResponse({'error': str(e)}, status_code=500)
return JSONResponse({'error': str(e) or e.__class__.__name__}, status_code=500)
@app.get('/api/settings/tokens', tags=["API Tokens"])
+8 -2
View File
@@ -227,10 +227,16 @@
throw new Error('Session expired');
}
const data = await res.json();
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data.error || 'Unknown error');
const detail = data.error || data.message || data.detail;
const msg = typeof detail === 'string'
? detail
: (Array.isArray(detail)
? detail.map(d => d.msg || JSON.stringify(d)).join('; ')
: (detail ? JSON.stringify(detail) : `HTTP ${res.status}`));
throw new Error(msg || 'Unknown error');
}
return data;
}
+23 -10
View File
@@ -586,6 +586,7 @@
const select = document.getElementById(selectId);
const group = groupId ? document.getElementById(groupId) : null;
select.innerHTML = '';
const vpnBases = new Set(['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard']);
if (serverId === '') {
if (group) group.style.display = 'none';
@@ -597,13 +598,14 @@
let count = 0;
for (const [key, info] of Object.entries(protocols)) {
if (info.installed) {
const opt = document.createElement('option');
opt.value = key;
opt.textContent = key === 'awg' ? 'AmneziaWG' : (key === 'awg2' ? 'AmneziaWG 2.0' : (key === 'awg_legacy' ? 'AWG Legacy' : (key === 'xray' ? 'Xray' : key.toUpperCase())));
select.appendChild(opt);
count++;
}
if (!info.installed) continue;
const base = key.split('__')[0];
if (!vpnBases.has(base)) continue;
const opt = document.createElement('option');
opt.value = key;
opt.textContent = key === 'awg' ? 'AmneziaWG' : (key === 'awg2' ? 'AmneziaWG 2.0' : (key === 'awg_legacy' ? 'AWG Legacy' : (key === 'xray' ? 'Xray' : (key === 'wireguard' ? 'WireGuard' : (key === 'telemt' ? 'Telemt' : key.toUpperCase())))));
select.appendChild(opt);
count++;
}
if (count > 0) {
@@ -765,15 +767,26 @@
const select = document.getElementById('ucExistingClient');
select.innerHTML = `<option value="">${_('loading')}</option>`;
if (!protocol) {
select.innerHTML = `<option value="">${_('no_protocols')}</option>`;
return;
}
try {
const data = await apiCall(`/api/servers/${serverId}/${protocol}/clients`);
const data = await apiCall(`/api/servers/${serverId}/${encodeURIComponent(protocol)}/clients`);
if (!data.clients || data.clients.length === 0) {
select.innerHTML = `<option value="">${_('no_free_conns')}</option>`;
return;
}
select.innerHTML = '';
select.innerHTML = '<option value="">' + _('select_existing_conn') + '</option>';
data.clients.forEach(c => {
select.innerHTML += `<option value="${c.id}">${c.name || 'Unnamed'} (${c.id.substring(0, 8)}...)</option>`;
const id = c.id || '';
const short = id ? (id.length > 8 ? id.substring(0, 8) + '...' : id) : '';
const label = `${c.name || 'Unnamed'}${short ? ' (' + short + ')' : ''}`;
const opt = document.createElement('option');
opt.value = id;
opt.textContent = label;
select.appendChild(opt);
});
} catch (err) {
select.innerHTML = `<option value="">${_('error')}: ${err.message}</option>`;
+1
View File
@@ -158,6 +158,7 @@
"conn_name_panel": "Connection name (in panel)",
"user_conns_title": "User connections",
"no_free_conns": "No free connections",
"select_connection": "Please select a connection",
"linking": "Linking...",
"conn_linked": "Connection linked",
"enabling": "Enabling",
+1
View File
@@ -156,6 +156,7 @@
"conn_name_panel": "نام اتصال (در پنل)",
"user_conns_title": "اتصال‌های کاربر",
"no_free_conns": "اتصال آزاد موجود نیست",
"select_connection": "لطفا یک اتصال انتخاب کنید",
"linking": "در حال اتصال...",
"conn_linked": "اتصال برقرار شد",
"enabling": "در حال فعال‌سازی",
+1
View File
@@ -156,6 +156,7 @@
"conn_name_panel": "Nom (dans le panneau)",
"user_conns_title": "Connexions utilisateur",
"no_free_conns": "Aucune connexion libre",
"select_connection": "Veuillez sélectionner une connexion",
"linking": "Liaison...",
"conn_linked": "Connexion liée",
"enabling": "Activation",
+1
View File
@@ -158,6 +158,7 @@
"conn_name_panel": "Имя подключения (в панели)",
"user_conns_title": "Подключения пользователя",
"no_free_conns": "Нет свободных подключений",
"select_connection": "Выберите подключение",
"linking": "Привязка...",
"conn_linked": "Подключение привязано",
"enabling": "Включение",
+1
View File
@@ -156,6 +156,7 @@
"conn_name_panel": "连接名称 (面板显示)",
"user_conns_title": "用户连接列表",
"no_free_conns": "无可用空闲连接",
"select_connection": "请选择一个连接",
"linking": "关联中...",
"conn_linked": "连接关联成功",
"enabling": "启用中",