mirror of
https://github.com/andrey271192/kaskad_web_vpn.git
synced 2026-09-20 11:55:35 +00:00
feat: kaskad memo site with Basic Auth
Flask + Docker, header support links, install/uninstall curl scripts, password file or ADMIN_PASSWORD. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.git
|
||||||
|
*.md
|
||||||
|
install.sh
|
||||||
|
uninstall.sh
|
||||||
|
.python-version
|
||||||
|
__pycache__
|
||||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Byte-compiled / cache
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.Python
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
|
*.local
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM python:3.12-slim-bookworm
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY app.py .
|
||||||
|
COPY templates ./templates
|
||||||
|
COPY static ./static
|
||||||
|
|
||||||
|
ENV PORT=8088
|
||||||
|
EXPOSE 8088
|
||||||
|
|
||||||
|
CMD ["sh", "-c", "exec gunicorn -w 2 -b 0.0.0.0:${PORT} app:app"]
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Andrey271192
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
71
README.md
Normal file
71
README.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# kaskad_web_vpn
|
||||||
|
|
||||||
|
Мини-сайт на Flask с **HTTP Basic Auth**: памятка про каскад **AmneziaWG**, без сторонней рекламы. В шапке — ссылки автора на поддержку (GitHub, Boosty, Ozon СБП, Telegram).
|
||||||
|
|
||||||
|
Репозиторий: [github.com/andrey271192/kaskad_web_vpn](https://github.com/andrey271192/kaskad_web_vpn)
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- Docker с доступом для текущего пользователя (или запуск скриптов от root).
|
||||||
|
|
||||||
|
## Установка одной командой
|
||||||
|
|
||||||
|
На сервере (порт хоста по умолчанию **8088**, логин **user1**, пароль генерируется и пишется в `/root/kaskad_web.initial-password`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/andrey271192/kaskad_web_vpn/main/install.sh | sudo bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Свой пароль и порт:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/andrey271192/kaskad_web_vpn/main/install.sh | sudo \
|
||||||
|
env ADMIN_PASSWORD='ваш_секрет' HOST_PORT=8443 bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Опционально ссылка на вашу веб-панель Amnezia (подставится на страницу):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/andrey271192/kaskad_web_vpn/main/install.sh | sudo \
|
||||||
|
env PANEL_URL='https://panel.example.com' bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Переменные окружения установки:
|
||||||
|
|
||||||
|
| Переменная | По умолчанию | Описание |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `HOST_PORT` | `8088` | Порт на хосте (`HOST_PORT:8088` в контейнере) |
|
||||||
|
| `BASIC_AUTH_USER` | `user1` | Логин Basic Auth |
|
||||||
|
| `ADMIN_PASSWORD` | — | Пароль; если не задан — берётся из файла или генерируется |
|
||||||
|
| `PASSWORD_FILE` | `/root/kaskad_web.initial-password` | Файл с паролем при автогенерации |
|
||||||
|
| `PANEL_URL` | пусто | URL панели для блока на главной |
|
||||||
|
| `KASKAD_REPO` | `andrey271192/kaskad_web_vpn` | Репозиторий GitHub |
|
||||||
|
| `KASKAD_REF` | `main` | Ветка |
|
||||||
|
| `KASKAD_INSTALL_ROOT` | `/opt` | Куда распаковать исходники для сборки |
|
||||||
|
|
||||||
|
## Удаление
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/andrey271192/kaskad_web_vpn/main/uninstall.sh | sudo bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Удалить ещё и Docker-образ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/andrey271192/kaskad_web_vpn/main/uninstall.sh | sudo env REMOVE_IMAGE=1 bash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Локально без Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv && source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
export BASIC_AUTH_PASSWORD='secret'
|
||||||
|
flask --app app run --host 0.0.0.0 --port 8088
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка без авторизации: `GET /health`.
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
MIT
|
||||||
56
app.py
Normal file
56
app.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
"""Мини-сайт про каскад AmneziaWG: только страница и HTTP Basic Auth."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
|
||||||
|
from flask import Flask, Response, render_template, request
|
||||||
|
|
||||||
|
USER = os.environ.get("BASIC_AUTH_USER", "user1").strip() or "user1"
|
||||||
|
PW = os.environ.get("BASIC_AUTH_PASSWORD", "").strip()
|
||||||
|
REALM = os.environ.get("BASIC_AUTH_REALM", "kaskad").strip() or "kaskad"
|
||||||
|
PANEL_URL = os.environ.get("PANEL_URL", "").strip()
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def check_auth(u: str, p: str) -> bool:
|
||||||
|
if not PW:
|
||||||
|
return False
|
||||||
|
return u == USER and p == PW
|
||||||
|
|
||||||
|
|
||||||
|
def requires_auth(view):
|
||||||
|
@functools.wraps(view)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
auth = request.authorization
|
||||||
|
if auth is None or not check_auth(auth.username or "", auth.password or ""):
|
||||||
|
return Response(
|
||||||
|
"Требуется вход\n",
|
||||||
|
401,
|
||||||
|
{"WWW-Authenticate": f'Basic realm="{REALM}"'},
|
||||||
|
mimetype="text/plain; charset=utf-8",
|
||||||
|
)
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health():
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
@requires_auth
|
||||||
|
def index():
|
||||||
|
return render_template("index.html", panel_url=PANEL_URL)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
port = int(os.environ.get("PORT", "8088"))
|
||||||
|
app.run(host="0.0.0.0", port=port, threaded=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
96
install.sh
Executable file
96
install.sh
Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Однострочная установка (пример в README): клон/скачивание с GitHub, сборка образа, запуск контейнера.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO="${KASKAD_REPO:-andrey271192/kaskad_web_vpn}"
|
||||||
|
REF="${KASKAD_REF:-main}"
|
||||||
|
IMAGE="${KASKAD_IMAGE:-kaskad-web-vpn:latest}"
|
||||||
|
CONTAINER="${KASKAD_CONTAINER:-kaskad-web-vpn}"
|
||||||
|
INSTALL_ROOT="${KASKAD_INSTALL_ROOT:-/opt}"
|
||||||
|
HOST_PORT="${HOST_PORT:-8088}"
|
||||||
|
BASIC_AUTH_USER="${BASIC_AUTH_USER:-user1}"
|
||||||
|
BASIC_AUTH_REALM="${BASIC_AUTH_REALM:-kaskad}"
|
||||||
|
PASSWORD_FILE="${PASSWORD_FILE:-/root/kaskad_web.initial-password}"
|
||||||
|
PANEL_URL="${PANEL_URL:-}"
|
||||||
|
|
||||||
|
need_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || {
|
||||||
|
echo "Нужна команда: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
need_cmd docker
|
||||||
|
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
echo "Docker недоступен (запустите сервис или добавьте пользователя в группу docker)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SRC="${INSTALL_ROOT}/kaskad_web_vpn_src"
|
||||||
|
mkdir -p "$(dirname "$SRC")"
|
||||||
|
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
ARCHIVE_URL="https://github.com/${REPO}/archive/refs/heads/${REF}.tar.gz"
|
||||||
|
echo "Скачивание ${ARCHIVE_URL} …"
|
||||||
|
curl -fsSL "$ARCHIVE_URL" -o "${TMP}/src.tar.gz"
|
||||||
|
tar -xzf "${TMP}/src.tar.gz" -C "$TMP"
|
||||||
|
EXTRACTED="$(find "$TMP" -maxdepth 1 -type d -name "*-${REF}" | head -n1)"
|
||||||
|
if [[ -z "$EXTRACTED" ]]; then
|
||||||
|
EXTRACTED="$(find "$TMP" -maxdepth 1 -type d ! -path "$TMP" | head -n1)"
|
||||||
|
fi
|
||||||
|
rm -rf "$SRC"
|
||||||
|
mkdir -p "$SRC"
|
||||||
|
shopt -s dotglob
|
||||||
|
mv "$EXTRACTED"/* "$SRC"/
|
||||||
|
|
||||||
|
echo "Сборка образа ${IMAGE} …"
|
||||||
|
docker build -t "$IMAGE" "$SRC"
|
||||||
|
|
||||||
|
if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then
|
||||||
|
echo "Удаление старого контейнера ${CONTAINER} …"
|
||||||
|
docker rm -f "$CONTAINER" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${ADMIN_PASSWORD:-}" ]]; then
|
||||||
|
BASIC_AUTH_PASSWORD="$ADMIN_PASSWORD"
|
||||||
|
elif [[ -f "$PASSWORD_FILE" ]] && grep -qs . "$PASSWORD_FILE"; then
|
||||||
|
BASIC_AUTH_PASSWORD="$(tr -d '\r\n' <"$PASSWORD_FILE")"
|
||||||
|
else
|
||||||
|
if command -v openssl >/dev/null 2>&1; then
|
||||||
|
BASIC_AUTH_PASSWORD="$(openssl rand -hex 16)"
|
||||||
|
else
|
||||||
|
BASIC_AUTH_PASSWORD="$(head -c 32 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 24)"
|
||||||
|
fi
|
||||||
|
umask 077
|
||||||
|
printf '%s\n' "$BASIC_AUTH_PASSWORD" >"$PASSWORD_FILE"
|
||||||
|
echo "Сгенерирован пароль Basic Auth, сохранён в ${PASSWORD_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$BASIC_AUTH_PASSWORD" ]]; then
|
||||||
|
echo "BASIC_AUTH_PASSWORD пуст — задайте ADMIN_PASSWORD или файл ${PASSWORD_FILE}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN_ARGS=(
|
||||||
|
-d
|
||||||
|
--name "$CONTAINER"
|
||||||
|
--restart unless-stopped
|
||||||
|
-p "${HOST_PORT}:8088"
|
||||||
|
-e "BASIC_AUTH_USER=${BASIC_AUTH_USER}"
|
||||||
|
-e "BASIC_AUTH_PASSWORD=${BASIC_AUTH_PASSWORD}"
|
||||||
|
-e "BASIC_AUTH_REALM=${BASIC_AUTH_REALM}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ -n "$PANEL_URL" ]]; then
|
||||||
|
RUN_ARGS+=(-e "PANEL_URL=${PANEL_URL}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run "${RUN_ARGS[@]}" "$IMAGE"
|
||||||
|
|
||||||
|
echo "Готово."
|
||||||
|
echo " URL: http://$(hostname -f 2>/dev/null || hostname):${HOST_PORT}/"
|
||||||
|
echo " Логин: ${BASIC_AUTH_USER}"
|
||||||
|
echo " Пароль: см. ${PASSWORD_FILE} (или переменная ADMIN_PASSWORD при установке)"
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Flask==3.0.3
|
||||||
|
gunicorn==23.0.0
|
||||||
227
static/styles.css
Normal file
227
static/styles.css
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #070b10;
|
||||||
|
--card: #111722;
|
||||||
|
--line: rgba(255, 255, 255, 0.08);
|
||||||
|
--text: #f4f7ff;
|
||||||
|
--muted: #94a3b8;
|
||||||
|
--accent: #7dd3fc;
|
||||||
|
--warn: #fbbf24;
|
||||||
|
font-family: "DM Sans", system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: radial-gradient(circle at 20% 20%, rgba(125, 211, 252, 0.07), transparent 40%),
|
||||||
|
var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.55;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
max-width: min(900px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1.25rem clamp(1rem, 3vw, 1.5rem) 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--card);
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header .support-line {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem 0.75rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header .support-title {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sep {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-blurb {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(1.35rem, 3.5vw, 1.75rem);
|
||||||
|
margin: 0 0 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: var(--accent);
|
||||||
|
margin: 0 0 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead {
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
max-width: 62ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-fold {
|
||||||
|
margin-top: 1rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--card);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-fold > summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.6rem;
|
||||||
|
user-select: none;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-fold > summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-fold[open] > summary {
|
||||||
|
border-bottom-color: var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fold-arrow {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
margin-top: 0.28rem;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 5px 0 5px 8px;
|
||||||
|
border-color: transparent transparent transparent var(--accent);
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-fold[open] > summary .fold-arrow {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fold-h {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fold-body {
|
||||||
|
padding: 0 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fold-body > *:first-child {
|
||||||
|
margin-top: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-warn {
|
||||||
|
border: 1px solid rgba(251, 191, 36, 0.35);
|
||||||
|
background: rgba(251, 191, 36, 0.06);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.65rem 0.85rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
code,
|
||||||
|
.mono {
|
||||||
|
font-family: ui-monospace, "JetBrains Mono", monospace;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-code {
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
padding: 0.12rem 0.35rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
max-width: 28rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-form label {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-form input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.55rem 0.65rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #0a0f16;
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.5rem 0.85rem;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgba(125, 211, 252, 0.12);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: rgba(125, 211, 252, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.out-box {
|
||||||
|
margin-top: 0.65rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #0a0f16;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: #cbd5f5;
|
||||||
|
min-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer.site-footer {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
144
templates/index.html
Normal file
144
templates/index.html
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Каскад AmneziaWG — памятка</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<header class="site-header">
|
||||||
|
<div class="support-line">
|
||||||
|
<span class="support-title">Поддержка автора</span>
|
||||||
|
<span class="sep" aria-hidden="true">·</span>
|
||||||
|
<a href="https://github.com/andrey271192/kaskad_web_vpn" target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||||
|
<span class="sep">·</span>
|
||||||
|
<a href="https://boosty.to/andrey27/donate" target="_blank" rel="noopener noreferrer">Boosty</a>
|
||||||
|
<span class="sep">·</span>
|
||||||
|
<a href="https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798" target="_blank" rel="noopener noreferrer">Ozon СБП</a>
|
||||||
|
<span class="sep">·</span>
|
||||||
|
<a href="https://t.me/lot_andrey" target="_blank" rel="noopener noreferrer">Telegram @lot_andrey</a>
|
||||||
|
</div>
|
||||||
|
<p class="support-blurb">Звезда репозиторию и донат помогают развивать утилиты. На этой странице нет сторонней рекламы.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p class="eyebrow">VPN · каскад</p>
|
||||||
|
<h1>Каскад для AmneziaWG</h1>
|
||||||
|
<p class="lead">
|
||||||
|
Краткая памятка: клиент подключается сначала к вашему <strong>промежуточному узлу</strong> (Endpoint),
|
||||||
|
а ключ и peer создаются на основном сервере AmneziaWG. Эта страница только объясняет шаги и может собрать текст-памятку;
|
||||||
|
генерация <span class="inline-code">.conf</span> выполняется в вашей <strong>админ-панели</strong> или вручную.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if panel_url %}
|
||||||
|
<p class="muted">Ваша панель: <a href="{{ panel_url }}">{{ panel_url }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<details class="panel-fold" open>
|
||||||
|
<summary>
|
||||||
|
<span class="fold-arrow" aria-hidden="true"></span>
|
||||||
|
<span class="fold-h">Что такое каскад</span>
|
||||||
|
</summary>
|
||||||
|
<div class="fold-body">
|
||||||
|
<p class="muted">
|
||||||
|
В конфиге клиента поле <span class="inline-code">Endpoint</span> указывает на IP или DNS узла «перед» VPS
|
||||||
|
(домашний роутер с пробросом UDP, другой VPS и т.д.). На этом узле нужен проброс порта на ваш основной сервер WireGuard/AWG.
|
||||||
|
Сервер AmneziaWG добавляет peer и выдаёт готовый <span class="inline-code">.conf</span>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="panel-fold" open>
|
||||||
|
<summary>
|
||||||
|
<span class="fold-arrow" aria-hidden="true"></span>
|
||||||
|
<span class="fold-h">Через панель Amnezia Admin</span>
|
||||||
|
</summary>
|
||||||
|
<div class="fold-body">
|
||||||
|
<p class="muted">
|
||||||
|
Если установлена <strong>Amnezia Admin WebUI</strong>, откройте блок <strong>«Новый клиент под каскад»</strong>:
|
||||||
|
укажите Endpoint (ваш публичный IP/DNS узла и при необходимости порт), затем скачайте <span class="inline-code">.conf</span>.
|
||||||
|
</p>
|
||||||
|
<p class="muted">
|
||||||
|
Репозиторий панели:
|
||||||
|
<a href="https://github.com/andrey271192/Amnezia_web" target="_blank" rel="noopener noreferrer">github.com/andrey271192/Amnezia_web</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="panel-fold">
|
||||||
|
<summary>
|
||||||
|
<span class="fold-arrow" aria-hidden="true"></span>
|
||||||
|
<span class="fold-h">Собрать памятку (локально в браузере)</span>
|
||||||
|
</summary>
|
||||||
|
<div class="fold-body">
|
||||||
|
<p class="muted">Данные не отправляются на сервер — только текст для копирования.</p>
|
||||||
|
<div class="grid-form">
|
||||||
|
<label for="ep-host">Endpoint (хост для клиента)</label>
|
||||||
|
<input id="ep-host" type="text" placeholder="203.0.113.50 или ddns.example.com" autocomplete="off">
|
||||||
|
|
||||||
|
<label for="ep-port">Порт Endpoint (если пусто — как у сервера)</label>
|
||||||
|
<input id="ep-port" type="number" min="1" max="65535" placeholder="51820" autocomplete="off">
|
||||||
|
|
||||||
|
<label for="memo-name">Имя клиента (произвольно)</label>
|
||||||
|
<input id="memo-name" type="text" placeholder="Телефон через узел X" autocomplete="off">
|
||||||
|
|
||||||
|
<div class="btn-row">
|
||||||
|
<button type="button" class="btn" id="btn-build">Собрать текст</button>
|
||||||
|
<button type="button" class="btn" id="btn-copy">Копировать</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre class="out-box" id="out-memo" aria-live="polite"></pre>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="panel-fold">
|
||||||
|
<summary>
|
||||||
|
<span class="fold-arrow" aria-hidden="true"></span>
|
||||||
|
<span class="fold-h">Проброс UDP</span>
|
||||||
|
</summary>
|
||||||
|
<div class="fold-body">
|
||||||
|
<p class="muted">
|
||||||
|
На промежуточном узле пробросьте UDP с выбранного порта на IP VPS и порт, где слушает ваш инстанс AmneziaWG.
|
||||||
|
Убедитесь, что фаервол не режет входящий UDP.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
Сервис: HTTP Basic Auth (realm «kaskad»). Установка одной ссылкой — см. README репозитория kaskad_web_vpn.
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function buildText() {
|
||||||
|
var host = (document.getElementById("ep-host").value || "").trim();
|
||||||
|
var port = (document.getElementById("ep-port").value || "").trim();
|
||||||
|
var name = (document.getElementById("memo-name").value || "").trim() || "клиент";
|
||||||
|
var epLine = host ? (host + (port ? ":" + port : "")) : "(укажите Endpoint)";
|
||||||
|
return (
|
||||||
|
"Памятка по каскаду\n" +
|
||||||
|
"— Имя: " + name + "\n" +
|
||||||
|
"— Endpoint для клиента: " + epLine + "\n" +
|
||||||
|
"— На промежуточном узле: проброс UDP на VPS и порт WG/AWG.\n" +
|
||||||
|
"— На основном сервере: создать peer (панель Amnezia Admin → «Новый клиент под каскад») и выдать .conf.\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
document.getElementById("btn-build").addEventListener("click", function () {
|
||||||
|
document.getElementById("out-memo").textContent = buildText();
|
||||||
|
});
|
||||||
|
document.getElementById("btn-copy").addEventListener("click", async function () {
|
||||||
|
var t = document.getElementById("out-memo").textContent || buildText();
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(t);
|
||||||
|
} catch (e) {
|
||||||
|
alert("Не удалось скопировать — выделите текст вручную.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
uninstall.sh
Executable file
16
uninstall.sh
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONTAINER="${KASKAD_CONTAINER:-kaskad-web-vpn}"
|
||||||
|
IMAGE="${KASKAD_IMAGE:-kaskad-web-vpn:latest}"
|
||||||
|
|
||||||
|
if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then
|
||||||
|
docker rm -f "$CONTAINER"
|
||||||
|
echo "Контейнер ${CONTAINER} удалён."
|
||||||
|
else
|
||||||
|
echo "Контейнер ${CONTAINER} не найден."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${REMOVE_IMAGE:-0}" == "1" ]]; then
|
||||||
|
docker rmi "$IMAGE" 2>/dev/null && echo "Образ ${IMAGE} удалён." || echo "Образ ${IMAGE} не найден или занят."
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user