Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions docs/ТЗ_CandyConnect_VPN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Техническое задание (ТЗ)
## Проект: CandyConnect VPN

## 1. Цель доработки
Обновить функциональность панели управления VPN в части протоколов OpenVPN и WireGuard, а также заменить тип подключения DNSTT на Amnezia. Доработки должны повысить стабильность выдачи клиентских конфигов, улучшить UX подключения WireGuard и добавить поддержку нового типа подключения в backend, frontend и инфраструктуре установки.

---

## 2. Проблемы текущей версии
1. **OpenVPN**
- Для новых клиентов не формируется/не выдается конфиг корректно.
- В карточке нового клиента отображается `openvpn.config`, но элемент не кликабелен и не позволяет скачать файл.
2. **WireGuard**
- Приватный ключ назначается автоматически сразу при создании клиента.
- Нет явного действия пользователя для получения QR-кода/данных авторизации после создания клиента.
3. **DNSTT**
- Протокол присутствует в системе, но должен быть полностью удален.
- Вместо DNSTT требуется внедрить **Amnezia**.

---

## 3. Объем работ

### 3.1 OpenVPN: исправление генерации и выдачи конфигов
#### Функциональные требования
1. При создании нового OpenVPN-клиента система обязана:
- генерировать валидный клиентский конфиг;
- сохранять его в хранилище (файловое/БД — согласно текущей архитектуре);
- отдавать ссылку/кнопку на скачивание в карточке клиента.
2. В карточке клиента элемент `openvpn.config` (или кнопка «Скачать конфиг») должен быть кликабельным.
3. По клику должен выполняться один из сценариев:
- скачивание файла с корректным MIME/именем файла;
- открытие модального окна с кнопкой скачивания (если так реализована UX-логика).
4. Для клиента, у которого конфиг отсутствует, должен отображаться понятный статус (например: «Конфиг не сгенерирован») и кнопка «Сгенерировать повторно» (опционально, но желательно).

#### Нефункциональные требования
- Время ответа API на выдачу ссылки/файла: до 2 сек при нормальной нагрузке.
- Корректная обработка ошибок (404, 500) с человекочитаемыми сообщениями в UI.

---

### 3.2 WireGuard: генерация QR по запросу
#### Функциональные требования
1. При создании нового WireGuard-клиента **не выполнять автогенерацию приватного ключа в интерфейсе как финальный шаг подключения**.
2. В профиле WireGuard-клиента после создания должна отображаться кнопка **«QR-код»**.
3. При нажатии кнопки:
- формируется (или загружается, если уже есть) конфиг клиента;
- генерируется QR-код на основе клиентского конфига;
- QR отображается в модальном окне/отдельном блоке;
- доступна кнопка «Скачать конфиг».
4. Должна быть защита от повторной гонки генерации (идемпотентность): повторные нажатия не должны ломать состояние клиента.

#### UX-требования
- Кнопка «QR-код» видна только для WireGuard-профилей.
- Во время генерации показывать индикатор загрузки.
- При ошибке — понятное сообщение (например: «Не удалось сгенерировать QR, попробуйте позже»).

---

### 3.3 Удаление DNSTT и внедрение Amnezia
#### Функциональные требования
1. Полностью удалить DNSTT из системы:
- backend-модели/enum/роуты;
- frontend-опции выбора типа подключения;
- скрипты установки и конфигурации;
- документацию и подсказки UI.
2. Добавить новый тип подключения **Amnezia**:
- в основные настройки системы;
- в настройки профилей;
- в API (создание/чтение/обновление профилей);
- в UI формы создания/редактирования.
3. Добавить установку необходимых пакетов для Amnezia в установочные скрипты (install/deploy/docker-образ при необходимости).
4. Реализовать проверку доступности зависимостей Amnezia после установки (health-check/валидация).

#### Инфраструктурные требования
- Новые пакеты должны устанавливаться без интерактивного ввода.
- Ошибки установки логируются и возвращаются в понятном виде.

---

## 4. Изменяемые компоненты (минимум)
- **Backend**: API клиентов, генерация конфигов, бизнес-логика протоколов, enum типов подключений.
- **Frontend (web-panel/client UI)**: карточка клиента, кнопки скачивания/QR, формы и настройки протоколов.
- **DevOps/Install**: install-скрипты, Dockerfile/docker-compose (если влияет), зависимости Amnezia.
- **Docs**: актуализация README/инструкций по подключению.

---

## 5. API/контракты (ожидаемые изменения)
1. Endpoint для скачивания OpenVPN-конфига должен стабильно возвращать файл.
2. Endpoint для WireGuard QR (новый или существующий) должен:
- принимать `clientId`;
- возвращать изображение/BASE64/URL QR;
- возвращать статус генерации.
3. В типах протоколов удалить `DNSTT`, добавить `AMNEZIA`.

> Точные пути endpoint и форматы определяются текущей архитектурой проекта, но обратная совместимость для неизмененных методов должна быть сохранена.

---

## 6. Критерии приемки

### 6.1 OpenVPN
- [ ] Новый OpenVPN-клиент получает рабочий конфиг.
- [ ] В карточке клиента есть кликабельная кнопка/ссылка скачивания конфига.
- [ ] Конфиг скачивается и импортируется в OpenVPN-клиент без ошибок формата.

### 6.2 WireGuard
- [ ] После создания WireGuard-клиента отображается кнопка «QR-код».
- [ ] При нажатии отображается валидный QR для авторизации/импорта.
- [ ] Пользователь может скачать конфиг из того же интерфейса.

### 6.3 Amnezia
- [ ] DNSTT отсутствует в UI, API и установке.
- [ ] Amnezia доступна в основных настройках и настройках профиля.
- [ ] Установочные скрипты ставят необходимые пакеты Amnezia.
- [ ] Создание профиля с типом Amnezia проходит успешно.

---

## 7. Тестирование
1. **Unit tests**
- генерация OpenVPN-конфига;
- генерация WireGuard-QR;
- валидация enum протоколов (без DNSTT, с Amnezia).
2. **Integration tests**
- создание клиента + скачивание OpenVPN-конфига;
- создание WireGuard-клиента + получение QR;
- создание/редактирование Amnezia-профиля.
3. **UI/E2E tests**
- кликабельность `openvpn.config`;
- отображение и открытие кнопки «QR-код»;
- отсутствие DNSTT и наличие Amnezia в селекторах.
4. **Smoke tests после деплоя**
- проверка установки зависимостей Amnezia;
- создание тестовых клиентов по каждому поддерживаемому типу.

---

## 8. Ограничения и риски
- Возможна миграция существующих данных, если DNSTT уже использовался в профилях.
- Возможны различия в пакетах Amnezia в зависимости от ОС/дистрибутива.
- Генерация QR должна учитывать безопасность: не логировать приватные ключи в открытом виде.

---

## 9. Ожидаемый результат
После выполнения доработок система CandyConnect VPN должна:
1. Корректно выдавать OpenVPN-конфиги новым клиентам.
2. Предоставлять WireGuard QR-код по нажатию кнопки в профиле клиента.
3. Полностью заменить DNSTT на Amnezia во всех слоях системы (UI/API/установка/настройки).
29 changes: 4 additions & 25 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ install_dependencies() {
ca-certificates gnupg \
openvpn easy-rsa \
strongswan strongswan-pki libcharon-extra-plugins \
xl2tpd wireguard wireguard-tools dante-server \
xl2tpd wireguard wireguard-tools amneziawg-tools dante-server \
openssh-server openssh-client

# Install Node.js (proper version)
Expand All @@ -97,30 +97,9 @@ install_dependencies() {
bash -c 'curl -sL https://raw.githubusercontent.com/XTLS/Xray-install/main/install-release.sh | bash -s -- install' || warn "Xray installation failed"
fi

# Install DNSTT-server (Robust install with checksum verification)
info "Installing DNSTT..."
if [ ! -f "/usr/local/bin/dnstt-server" ]; then
DURL="https://dnstt.network"
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then DARCH="amd64"; elif [ "$ARCH" = "aarch64" ]; then DARCH="arm64"; else DARCH="386"; fi
DFILE="dnstt-server-linux-${DARCH}"

info "Downloading DNSTT from ${DURL}/${DFILE}..."
curl -L -o "/tmp/${DFILE}" "${DURL}/${DFILE}"
curl -L -s -o "/tmp/SHA256SUMS" "${DURL}/SHA256SUMS"

cd /tmp
if sha256sum -c <(grep "${DFILE}" SHA256SUMS) 2>/dev/null; then
mv "/tmp/${DFILE}" "/usr/local/bin/dnstt-server"
chmod +x "/usr/local/bin/dnstt-server"
info "DNSTT verified and installed"
else
warn "DNSTT checksum verification failed! Attempting insecure install..."
wget -qO /usr/local/bin/dnstt-server "${DURL}/${DFILE}" || warn "DNSTT download failed"
chmod +x /usr/local/bin/dnstt-server
fi
cd - >/dev/null
fi
info "Installing Amnezia tools..."
command -v amneziawg >/dev/null 2>&1 || warn "amneziawg binary not found; ensure repository package is available for your distro"


# Enable and start Redis
systemctl enable redis-server || true
Expand Down
2 changes: 1 addition & 1 deletion server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,6 @@ def _load_or_create_jwt_secret() -> str:

# ── Protocols ──
SUPPORTED_PROTOCOLS = [
"v2ray", "wireguard", "openvpn", "ikev2", "l2tp", "dnstt",
"v2ray", "wireguard", "openvpn", "ikev2", "l2tp", "amnezia",
"slipstream", "trusttunnel",
]
18 changes: 9 additions & 9 deletions server/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ async def init_db():
await r.hset(K_CONFIGS, section, json.dumps(data))

# Default core statuses
for proto in ["v2ray", "wireguard", "openvpn", "ikev2", "l2tp", "dnstt", "slipstream", "trusttunnel"]:
for proto in ["v2ray", "wireguard", "openvpn", "ikev2", "l2tp", "amnezia", "slipstream", "trusttunnel"]:
if not await r.hexists(K_CORE_STATUS, proto):
# All protocols except slipstream and trusttunnel are running by default
status = "running" if proto not in ["slipstream", "trusttunnel"] else "stopped"
Expand All @@ -124,7 +124,7 @@ async def init_db():
"comment": "Default administrator client",
"enabled": True,
"protocols": {p: (p not in ["slipstream", "trusttunnel"]) for p in [
"v2ray", "wireguard", "openvpn", "ikev2", "l2tp", "dnstt", "slipstream", "trusttunnel"
"v2ray", "wireguard", "openvpn", "ikev2", "l2tp", "amnezia", "slipstream", "trusttunnel"
]}
}
await create_client(admin_client_data)
Expand Down Expand Up @@ -211,12 +211,12 @@ def _default_core_configs() -> dict:
"remote_range": "10.20.0.10-10.20.0.250",
"dns": "1.1.1.1", "mtu": 1400, "mru": 1400,
},
"dnstt": {
"listen_port": 5300,
"domain": f"dns.{PANEL_DOMAIN}",
"amnezia": {
"port": 51830,
"transport": "udp",
"obfuscation": "on",
"domain": f"amnezia.{PANEL_DOMAIN}",
"public_key": "",
"tunnel_mode": "ssh",
"mtu": 1232,
},
"slipstream": {
"port": 8388, "method": "aes-256-cfb",
Expand Down Expand Up @@ -365,7 +365,7 @@ async def create_client(data: dict) -> dict:
"created_at": now,
"expires_at": _calc_expiry(now, data.get("time_limit", {"mode": "days", "value": 30})),
"protocols": data.get("protocols", {p: True for p in [
"v2ray", "wireguard", "openvpn", "ikev2", "l2tp", "dnstt", "slipstream", "trusttunnel"
"v2ray", "wireguard", "openvpn", "ikev2", "l2tp", "amnezia", "slipstream", "trusttunnel"
]}),
"protocol_data": data.get("protocol_data", {}),
"last_connected_ip": None,
Expand Down Expand Up @@ -413,7 +413,7 @@ async def delete_client(client_id: str) -> bool:
await r.hdel(K_CLIENTS, client_id)
await r.hdel(K_CLIENT_IDX, client.get("username", ""))
# Clean up traffic keys
for proto in ["v2ray", "wireguard", "openvpn", "ikev2", "l2tp", "dnstt", "slipstream", "trusttunnel"]:
for proto in ["v2ray", "wireguard", "openvpn", "ikev2", "l2tp", "amnezia", "slipstream", "trusttunnel"]:
await r.hdel(K_TRAFFIC, f"{client_id}:{proto}")
await _add_log("INFO", "System", f"Client '{client.get('username', '')}' deleted")
return True
Expand Down
59 changes: 59 additions & 0 deletions server/protocols/amnezia.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""CandyConnect - Amnezia Protocol Manager."""
import time
from protocols.base import BaseProtocol
from database import get_core_status, set_core_status, add_log


class AmneziaProtocol(BaseProtocol):
PROTOCOL_ID = "amnezia"
PROTOCOL_NAME = "Amnezia"
DEFAULT_PORT = 51830

async def install(self) -> bool:
try:
await add_log("INFO", self.PROTOCOL_NAME, "Installing Amnezia dependencies...")
await self._apt_install("amneziawg-tools")
return True
except Exception as e:
await add_log("ERROR", self.PROTOCOL_NAME, f"Installation error: {e}")
return False

async def start(self) -> bool:
status = await get_core_status(self.PROTOCOL_ID)
await set_core_status(self.PROTOCOL_ID, {
"status": "running",
"pid": status.get("pid"),
"started_at": int(time.time()),
"version": status.get("version", ""),
})
await add_log("INFO", self.PROTOCOL_NAME, "Amnezia marked as running")
return True

async def stop(self) -> bool:
status = await get_core_status(self.PROTOCOL_ID)
await set_core_status(self.PROTOCOL_ID, {
"status": "stopped",
"pid": None,
"started_at": None,
"version": status.get("version", ""),
})
await add_log("INFO", self.PROTOCOL_NAME, "Amnezia stopped")
return True

async def is_running(self) -> bool:
status = await get_core_status(self.PROTOCOL_ID)
return status.get("status") == "running"

async def add_client(self, username: str, client_data: dict) -> dict:
return {"username": username, "status": "ready"}

async def remove_client(self, username: str, protocol_data: dict):
return None

async def get_client_config(self, username: str, server_ip: str, protocol_data: dict, config_id: str = None) -> dict:
return {
"type": "amnezia",
"server": server_ip,
"port": self.DEFAULT_PORT,
"username": username,
}
10 changes: 5 additions & 5 deletions server/protocols/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from protocols.openvpn import OpenVPNProtocol
from protocols.ikev2 import IKEv2Protocol
from protocols.l2tp import L2TPProtocol
from protocols.dnstt import DNSTTProtocol
from protocols.amnezia import AmneziaProtocol
from protocols.slipstream import SlipStreamProtocol
from protocols.trusttunnel import TrustTunnelProtocol

Expand All @@ -39,7 +39,7 @@ def __init__(self) -> None:
"openvpn": OpenVPNProtocol(),
"ikev2": IKEv2Protocol(),
"l2tp": L2TPProtocol(),
"dnstt": DNSTTProtocol(),
"amnezia": AmneziaProtocol(),
"slipstream": SlipStreamProtocol(),
"trusttunnel": TrustTunnelProtocol(),
}
Expand All @@ -53,7 +53,7 @@ async def auto_start_protocols(self) -> None:
logger = logging.getLogger("candyconnect")

# 1. Start Protocols
for pid in ["v2ray", "wireguard", "openvpn", "ikev2", "l2tp", "dnstt"]:
for pid in ["v2ray", "wireguard", "openvpn", "ikev2", "l2tp", "amnezia"]:
try:
installed = await self.install_protocol(pid)
started = await self.start_protocol(pid)
Expand Down Expand Up @@ -283,8 +283,8 @@ async def _get_protocol_port(self, pid: str) -> int:
return int(cfg.get("port", default_port))
elif pid == "l2tp":
return int(cfg.get("port", default_port))
elif pid == "dnstt":
return int(cfg.get("listen_port", default_port))
elif pid == "amnezia":
return int(cfg.get("port", default_port))
elif pid in ("slipstream", "trusttunnel"):
return int(cfg.get("port", default_port))
elif pid == "v2ray":
Expand Down
Loading