Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28986677eb | ||
|
|
af4c269438 | ||
|
|
528d799edc | ||
|
|
31978f22f0 | ||
|
|
c5e9e45141 | ||
|
|
51ec84db6c | ||
|
|
d030344b00 |
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,3 +1,7 @@
|
||||
## 1.4.9 — 2026-11-03
|
||||
- Команда secondary сама подставляет подсеть главной; install.sh в сводке показывает реальные ключ API и пароль.
|
||||
- Панель «Серверы»: сверху блок «Как добавить второй сервер» (по-русски, команда secondary, предупреждение про независимые панели и подсети); ручное добавление свёрнуто. install.sh: предупреждение перед вопросом о подсети; подсеть `10.25.11.0/16` → `10.25.11.0/24` вместо молчаливого `10.25.0.0/16`.
|
||||
|
||||
# Changelog — PCA Phobos
|
||||
|
||||
Каналы: **stable** (публичный, без токена) · **beta** / **dev** (приватный repo, токен подписчика).
|
||||
@@ -5,6 +9,9 @@
|
||||
|
||||
---
|
||||
|
||||
## 1.4.10 — 2026-11-03
|
||||
- Трафик за месяц на клиента (суммарно по всем серверам, обнуляется 1-го числа): колонка в «Клиентах», /api/traffic, история по месяцам. Secondary отдают счётчики в /api/health — secondary обновить, иначе их трафик не учитывается.
|
||||
|
||||
## Как обновиться после фиксов
|
||||
|
||||
```bash
|
||||
@@ -19,6 +26,10 @@ phobos-update beta # или: phobos-update dev
|
||||
|
||||
---
|
||||
|
||||
## 1.4.8 — сторож роутера не трогает чужие туннели
|
||||
- Исправлено: если сторож не находил интерфейс Phobos (удалён или переименован), он брал Wireguard3 по умолчанию и прописывал туда пир Phobos — ломался посторонний WireGuard-туннель. Теперь он просто пропускает проверку.
|
||||
- Роутерам: обновить скрипты из панели или переустановить клиента; серверу — `phobos-update stable`.
|
||||
|
||||
## 1.4.7 — роутеры Keenetic сами переключаются между серверами
|
||||
- Исправлено: после переключения на другой сервер туннель на роутере переставал работать — пир прописывался на порт обфускатора 13255, а он мог слушать другой. Порт теперь берётся из `wg-obfuscator.conf`.
|
||||
- Исправлено: проверка и обновление конфига по расписанию на Keenetic не запускались — задачи писались в `/opt/etc/crontab`, который демон не читает. Теперь busybox `crond` и crontab пользователя root.
|
||||
|
||||
122
app.py
122
app.py
@@ -802,6 +802,7 @@ _last_fanout = 0
|
||||
_last_peer_sync = 0
|
||||
server_stats_cache = {}
|
||||
server_handshakes_cache = {}
|
||||
server_transfer_cache = {} # server ip -> {pub: [rx, tx]} (from secondary /api/health)
|
||||
|
||||
|
||||
def count_client_peers():
|
||||
@@ -889,6 +890,7 @@ def get_remote_stats(server):
|
||||
raise Exception("no response")
|
||||
# cache handshakes so page renders never block on remote HTTP
|
||||
server_handshakes_cache[server.get("ip", "")] = data.get("handshakes", {}) or {}
|
||||
server_transfer_cache[server.get("ip", "")] = data.get("transfer", {}) or {}
|
||||
# Filter peers: only count known client public keys
|
||||
clients = get_clients()
|
||||
client_pubs = {c.get("public_key", "") for c in clients}
|
||||
@@ -1751,6 +1753,71 @@ def finish_legacy_update():
|
||||
finish_legacy_update()
|
||||
threading.Thread(target=rate_sampler, daemon=True).start()
|
||||
|
||||
# ---- Monthly traffic per client, summed over all servers; resets on the 1st ----
|
||||
# WireGuard counters live per server and drop to 0 on wg restart/reboot, so we add deltas.
|
||||
TRAFFIC_FILE = f"{PANEL_DIR}/traffic.json"
|
||||
_traffic_lock = threading.Lock()
|
||||
|
||||
|
||||
def _traffic_load():
|
||||
try:
|
||||
with open(TRAFFIC_FILE) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _local_transfer():
|
||||
out = {}
|
||||
try:
|
||||
for line in subprocess.check_output(["wg", "show", "wg0", "dump"], text=True, timeout=5).strip().split("\n")[1:]:
|
||||
p = line.split("\t")
|
||||
if len(p) >= 7:
|
||||
out[p[0]] = [int(p[5]), int(p[6])]
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def traffic_sample():
|
||||
month = datetime.now().strftime("%Y-%m")
|
||||
with _traffic_lock:
|
||||
d = _traffic_load()
|
||||
if d.get("month") != month:
|
||||
if d.get("usage"):
|
||||
d.setdefault("history", {})[d.get("month", "?")] = d["usage"]
|
||||
d["month"], d["usage"] = month, {}
|
||||
prev, usage = d.setdefault("prev", {}), d.setdefault("usage", {})
|
||||
pub2cid = {c.get("public_key"): c.get("client_id") for c in get_clients() if c.get("public_key")}
|
||||
srcs = {SERVER_IP: _local_transfer()}
|
||||
srcs.update(server_transfer_cache)
|
||||
for ip, counters in srcs.items():
|
||||
for pub, (rx, tx) in counters.items():
|
||||
key, old, cid = ip + "|" + pub, prev.get(ip + "|" + pub), pub2cid.get(pub)
|
||||
prev[key] = [rx, tx]
|
||||
if old is None or not cid: # first sight = baseline only
|
||||
continue
|
||||
u = usage.setdefault(cid, [0, 0])
|
||||
u[0] += rx - old[0] if rx >= old[0] else rx # counter went down = wg restarted
|
||||
u[1] += tx - old[1] if tx >= old[1] else tx
|
||||
tmp = TRAFFIC_FILE + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(d, f)
|
||||
os.replace(tmp, TRAFFIC_FILE)
|
||||
|
||||
|
||||
def traffic_sampler():
|
||||
time.sleep(20)
|
||||
while True:
|
||||
try:
|
||||
traffic_sample()
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(60)
|
||||
|
||||
|
||||
threading.Thread(target=traffic_sampler, daemon=True).start()
|
||||
|
||||
|
||||
PAGE = """<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
@@ -2294,6 +2361,27 @@ def _terminal_button(url, title):
|
||||
f'style="background:#334155" title="{_html_attr(title)}">🖥️</a>')
|
||||
|
||||
|
||||
def _fmt_bytes(n):
|
||||
for u in ("B", "KiB", "MiB", "GiB", "TiB"):
|
||||
if n < 1024 or u == "TiB":
|
||||
return f"{n:.2f} {u}" if u != "B" else f"{int(n)} B"
|
||||
n /= 1024
|
||||
|
||||
|
||||
def _traffic_cell(u):
|
||||
if not u:
|
||||
return "—"
|
||||
return f"↓ {_fmt_bytes(u[1])} · ↑ {_fmt_bytes(u[0])}" # server tx = client download
|
||||
|
||||
|
||||
@app.route("/api/traffic")
|
||||
@auth_required
|
||||
def api_traffic():
|
||||
"""Monthly traffic per client: current month + history ({client: [rx, tx]}, server-side bytes)."""
|
||||
d = _traffic_load()
|
||||
return jsonify({"month": d.get("month"), "usage": d.get("usage", {}), "history": d.get("history", {})})
|
||||
|
||||
|
||||
@app.route("/clients", methods=["GET", "POST"])
|
||||
@auth_required
|
||||
def clients_page():
|
||||
@@ -2601,6 +2689,7 @@ def clients_page():
|
||||
<button class="btn btn-sm" style="background:#475569" type="submit">{tr("Имя","Name")}</button>
|
||||
</form>
|
||||
"""
|
||||
_tusage = _traffic_load().get("usage", {}) if "_tusage" not in dir() else _tusage
|
||||
lim = str(limits.get(cid, "") or "")
|
||||
rate = client_rates.get(pub)
|
||||
speed = (f'<span class="cspeed" title="{tr("Скорость сейчас","Current speed")}">↓{rate[0]:.1f} ↑{rate[1]:.1f} {tr("Мбит/с","Mbit/s")}</span>' if rate else "")
|
||||
@@ -2613,7 +2702,7 @@ def clients_page():
|
||||
<button class="btn btn-primary btn-sm" type="submit">{set_lbl}</button>
|
||||
</form>"""
|
||||
rows += f"""<div class="ccard">
|
||||
<div class="ctop"><span class="cname">{client_cell}</span> <span class="cip">{ip}</span> {status} {expiry_badge} {f'<span class="badge badge-lock">≤{_html_attr(lim)} Mbit</span>' if lim and lim != "0" else ""} {speed}</div>
|
||||
<div class="ctop"><span class="cname">{client_cell}</span> <span class="cip">{ip}</span> {status} {expiry_badge} <span class="cspeed" title="{tr('Трафик за месяц (суммарно по всем серверам)','Traffic this month (all servers)')}">{_traffic_cell(_tusage.get(cid))}</span> {f'<span class="badge badge-lock">≤{_html_attr(lim)} Mbit</span>' if lim and lim != "0" else ""} {speed}</div>
|
||||
<div class="cctl">{server_cell}{expiry_cell}{limit_cell}{rename_box}</div>
|
||||
<div class="cact">{actions_cell}</div>
|
||||
</div>"""
|
||||
@@ -4479,6 +4568,26 @@ def servers_page():
|
||||
{nav('servers')}
|
||||
{msg}
|
||||
|
||||
<div class="card" style="border:2px solid var(--accent)">
|
||||
<h2 style="font-size:1.25em">➕ {tr("Как добавить второй сервер", "How to add a second server")}</h2>
|
||||
<p style="font-size:1em;font-weight:600;margin:6px 0">{tr("Сервер управления один — эта панель. Второй VPS ставится как «дополнительный» (secondary) одной командой.", "There is one management server — this panel. The second VPS is installed as a secondary with one command.")}</p>
|
||||
<ol style="margin:8px 0 8px 20px;line-height:1.6;font-size:.95em">
|
||||
<li>{tr("Возьмите чистый VPS (Ubuntu/Debian). Если на нём уже стоял Phobos целиком — сначала удалите его (uninstall.sh).", "Take a clean VPS (Ubuntu/Debian). If a full Phobos was installed there, uninstall it first (uninstall.sh).")}</li>
|
||||
<li>{tr("Зайдите на него по SSH под root и выполните команду ниже.", "SSH to it as root and run the command below.")}</li>
|
||||
<li><b>{tr("Подсеть уже подставлена в команду (SERVER_WG_IPV4_NETWORK) — она такая же, как у этой панели, вопроса не будет.", "The subnet is already in the command (SERVER_WG_IPV4_NETWORK) — it is the same as on this panel, no prompt.")}</b> {tr("Не меняйте её и не вводите другую!", "Do not change it or enter another one!")}</li>
|
||||
<li>{tr("Сервер сам появится в списке ниже, клиенты скопируются на него автоматически. Кнопка «Добавить сервер» не нужна.", "The server appears in the list below by itself and clients are copied to it automatically. The “Add Server” button is not needed.")}</li>
|
||||
<li>{tr("Назначить клиенту сервер: Клиенты → «Редактировать» → «Сервер». Наши приложения (роутер, iOS, Android, Windows 1.4.5+) переключатся сами; другие клиенты — только повторным импортом ссылки.", "Assign a server to a client: Clients → Edit → Server. Our apps (router, iOS, Android, Windows 1.4.5+) switch by themselves; other clients need the link re-imported.")}</li>
|
||||
</ol>
|
||||
<div style="display:flex;gap:8px;align-items:flex-start">
|
||||
<code id="secCmd" style="background:var(--bg);padding:8px 12px;border-radius:6px;display:block;font-size:.82em;word-break:break-all;flex:1">SERVER_WG_IPV4_NETWORK={server_wg_network()} MAIN_PROTO={panel_scheme()} MAIN_SERVER={SERVER_IP} MAIN_PORT={panel_port()} MAIN_API_KEY={api_key} bash <(curl -kfsSL {panel_base_url()}/server/secondary-setup.sh)</code>
|
||||
<button class="btn btn-primary btn-sm" type="button" onclick="var x=document.createElement('textarea');x.value=document.getElementById('secCmd').textContent;document.body.appendChild(x);x.select();document.execCommand('copy');document.body.removeChild(x);this.textContent='{tr("Скопировано","Copied")}'">📋 {tr("Копировать","Copy")}</button>
|
||||
</div>
|
||||
<div style="margin-top:12px;padding:10px 12px;border-radius:8px;background:rgba(245,158,11,.15);border:1px solid #f59e0b;font-size:.9em;line-height:1.5">
|
||||
<b>⚠ {tr("Не ставьте второй сервер полным install.sh!", "Do not install the second server with the full install.sh!")}</b><br>
|
||||
{tr("Полный install.sh создаёт ОТДЕЛЬНУЮ панель со своими клиентами. Подсеть там придётся указывать свою (например 10.25.11.0/24, иначе у клиентов будут одинаковые IP и второй туннель на роутере не поднимется), а смена сервера у клиента между двумя панелями НЕ работает — это разные клиенты. Нужна смена и назначение серверов — ставьте только командой выше, подсеть по умолчанию (Enter).", "The full install.sh creates a SEPARATE panel with its own clients. You would have to enter your own subnet there (e.g. 10.25.11.0/24, otherwise clients get identical IPs and the second router tunnel will not come up), and switching a client's server between two panels does NOT work — they are different clients. If you need server assignment and switching, use only the command above with the default subnet (Enter).")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>{tr("Панель серверов", "Server Dashboard")} {hlp("Живая статистика всех VPN-серверов. Peers = активные WG-подключения. Assigned = клиенты, у которых этот сервер основной. Фоновый монитор опрашивает каждые " + str(s.get('monitor_interval',30)) + " сек и шлёт оповещения в Telegram при падении/восстановлении сервера.", "Live stats for all VPN servers. Peers = active WG connections. Assigned = clients whose primary is this server. Background monitor polls every " + str(s.get('monitor_interval',30)) + "s and sends Telegram alerts on server up/down.")}</h2>
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap">{stats_cards}</div>
|
||||
@@ -4498,6 +4607,7 @@ def servers_page():
|
||||
<button class="btn btn-primary" type="submit" onclick="return confirm('{tr("Отправить конфиг на все роутеры?","Push config to all routers?")}')">Push All</button></form>
|
||||
{hlp("Принудительно отправить конфиг на роутеры с прямым SSH. Роутеры за NAT это игнорируют и сами подтягивают конфиг за ~15 сек — обычно эта кнопка не нужна.", "Force-push config to routers reachable by SSH. NAT routers ignore it and auto-pull within ~15s — you usually do not need this.")}
|
||||
</div>
|
||||
<details style="margin-top:16px"><summary style="cursor:pointer;color:var(--muted)">{tr("Сервер уже поставлен, но не появился в списке? Добавить вручную", "Server installed but not in the list? Add manually")}</summary>
|
||||
<form method="post" class="form-row" style="margin-top:16px;flex-wrap:wrap">
|
||||
<input type="hidden" name="action" value="add">
|
||||
<input type="text" name="ip" placeholder="{tr('IP сервера','Server IP')}" required>
|
||||
@@ -4508,6 +4618,7 @@ def servers_page():
|
||||
<button class="btn btn-primary" type="submit">{tr("Добавить сервер","Add Server")}</button>
|
||||
{hlp("Добавить новый VPN-сервер в пул резервирования. Если secondary ставился командой ниже, API key можно оставить пустым — будет использован главный ключ панели. 8444 — это порт mini-API, не ключ.", "Add a new VPN server to the failover pool. If the secondary was installed with the command below, API key may be left blank — the panel main key will be used. 8444 is the mini-API port, not the key.")}
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -4527,7 +4638,7 @@ def servers_page():
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Server SSH Access</h2>
|
||||
<h2>{tr("SSH-доступ к серверам","Server SSH Access")}</h2>
|
||||
<p style="color:var(--muted);font-size:.85em;margin-bottom:12px">SSH credentials for secondary servers. Used by panel to add/remove WG peers when switching client servers. Main server uses local commands.</p>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="save_server_access">
|
||||
@@ -4546,7 +4657,7 @@ def servers_page():
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>API Key</h2>
|
||||
<h2>{tr("API-ключ","API Key")}</h2>
|
||||
<form method="post" class="form-row">
|
||||
<input type="hidden" name="action" value="set_api_key">
|
||||
<input type="text" name="server_api_key" value="{api_key}" placeholder="API key" style="width:300px">
|
||||
@@ -4554,11 +4665,6 @@ def servers_page():
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Deploy Secondary Server</h2>
|
||||
<p style="color:var(--muted);font-size:.85em;margin-bottom:8px">One command deploys WG + obfuscator + mini-API on a new VPS. Auto-registers in panel. Run via SSH on new VPS:</p>
|
||||
<code style="background:var(--bg);padding:8px 12px;border-radius:6px;display:block;font-size:.82em;word-break:break-all">MAIN_PROTO={panel_scheme()} MAIN_SERVER={SERVER_IP} MAIN_PORT={panel_port()} MAIN_API_KEY={api_key} bash <(curl -kfsSL {panel_base_url()}/server/secondary-setup.sh)</code>
|
||||
</div>
|
||||
</div>"""
|
||||
return render(html)
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -96,8 +96,10 @@ FAIL_COUNT=$(cat "$STATE/fail_count" 2>/dev/null || echo "0")
|
||||
PRIMARY_CHECK_TS=$(cat "$STATE/primary_check_ts" 2>/dev/null || echo "0")
|
||||
|
||||
# ── Detect Phobos WG interface (non-fatal, fallback to Wireguard3) ──
|
||||
# Нет своего интерфейса — выходим: запасной Wireguard3 бывает чужим туннелем.
|
||||
detect_wg_interface || {
|
||||
log "WARN: detect failed, using default $WG_IF"
|
||||
log "ERROR: own WG interface not found, skip"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Get handshake age via ndmc ──
|
||||
|
||||
19
install.sh
19
install.sh
@@ -42,6 +42,7 @@ PY
|
||||
fi
|
||||
# Подсеть клиентов: env SERVER_WG_IPV4_NETWORK или вопрос в терминале (Enter = 10.25.0.0/16)
|
||||
if [ -z "${SERVER_WG_IPV4_NETWORK:-}" ] && [ -r /dev/tty ] && [ -t 1 ]; then
|
||||
printf '\n!!! Это ВТОРОЙ сервер к уже работающей панели? Тогда прервите (Ctrl+C) и ставьте командой из панели\n «Серверы -> Как добавить второй сервер» (secondary-setup.sh), подсеть там по умолчанию.\n Своя подсеть нужна только для НЕЗАВИСИМОЙ панели (например 10.25.11.0/24, не пересекается с первой);\n смена сервера у клиентов между независимыми панелями не работает.\n\n' > /dev/tty
|
||||
printf 'Подсеть VPN-клиентов [10.25.0.0/16]: ' > /dev/tty
|
||||
read -r -t 60 SERVER_WG_IPV4_NETWORK < /dev/tty || true
|
||||
fi
|
||||
@@ -49,9 +50,16 @@ SERVER_WG_IPV4_NETWORK="${SERVER_WG_IPV4_NETWORK:-10.25.0.0/16}"
|
||||
SERVER_WG_IPV4_NETWORK="$(python3 - "$SERVER_WG_IPV4_NETWORK" <<'PY'
|
||||
import ipaddress, sys
|
||||
try:
|
||||
net = ipaddress.ip_network(sys.argv[1].strip(), strict=False)
|
||||
net = ipaddress.ip_network(sys.argv[1].strip(), strict=True)
|
||||
except ValueError:
|
||||
sys.exit("ERROR: bad subnet " + sys.argv[1])
|
||||
# «10.25.11.0/16» — биты хоста заданы: раньше молча превращалось в 10.25.0.0/16
|
||||
# (две установки с одинаковой подсетью → одинаковые IP клиентов, конфликт на роутере).
|
||||
# Считаем, что нужна подсеть этого адреса, и берём /24.
|
||||
try:
|
||||
net = ipaddress.ip_network(sys.argv[1].strip().split("/")[0] + "/24", strict=False)
|
||||
sys.stderr.write("NOTICE: subnet %s has host bits set, using %s\n" % (sys.argv[1], net))
|
||||
except ValueError:
|
||||
sys.exit("ERROR: bad subnet " + sys.argv[1])
|
||||
if net.version != 4 or not net.is_private or net.prefixlen > 29:
|
||||
sys.exit("ERROR: subnet must be private IPv4, /8../29 (e.g. 10.25.1.0/24)")
|
||||
print(net)
|
||||
@@ -363,6 +371,13 @@ sleep 2
|
||||
mkdir -p "$PANEL_DIR"
|
||||
echo "$CHANNEL" > "$PANEL_DIR/.channel" 2>/dev/null || true
|
||||
|
||||
# при переустановке settings.json остаётся старый — в сводке показываем то, что реально в панели
|
||||
_S="$PANEL_DIR/settings.json"
|
||||
if [ -f "$_S" ]; then
|
||||
API_KEY="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("server_api_key") or sys.argv[2])' "$_S" "$API_KEY" 2>/dev/null || echo "$API_KEY")"
|
||||
PANEL_PASS="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("admin_pass") or sys.argv[2])' "$_S" "$PANEL_PASS" 2>/dev/null || echo "$PANEL_PASS")"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Installation complete"
|
||||
|
||||
@@ -460,10 +460,11 @@ deploy_health_monitor() {
|
||||
# (vixie, PROCS=cron) на Keenetic задачи не выполнял — заменяем (01.11.2026)
|
||||
local I=/opt/etc/init.d/S10cron
|
||||
if [ ! -x $I ] || grep -q "^PROCS=cron$" $I; then
|
||||
$I stop >/dev/null 2>&1
|
||||
[ -x $I ] && $I stop </dev/null >/dev/null 2>&1
|
||||
printf '#!/bin/sh\n\nENABLED=yes\nPROCS=crond\nARGS="-c /opt/var/spool/cron/crontabs -b"\nPREARGS=""\nDESC=$PROCS\nPATH=/opt/sbin:/opt/bin:/usr/sbin:/usr/bin:/sbin:/bin\n\n. /opt/etc/init.d/rc.func\n' > $I
|
||||
chmod +x $I
|
||||
fi
|
||||
[ -x /opt/sbin/crond ] || command -v crond >/dev/null 2>&1 || log " ВНИМАНИЕ: crond не найден — health/pull по расписанию работать не будут (opkg install cron или busybox с crond)"
|
||||
mkdir -p /opt/var/spool/cron/crontabs
|
||||
# 01.11.2026: демоны cron на Keenetic (busybox crond, Entware cron) читают crontab
|
||||
# пользователя root, а не /opt/etc/crontab — туда писать бесполезно
|
||||
|
||||
@@ -69,11 +69,16 @@ def health():
|
||||
out = subprocess.check_output(["wg", "show", "wg0", "dump"], text=True, timeout=5)
|
||||
peer_keys = []
|
||||
handshakes = {}
|
||||
transfer = {}
|
||||
for line in out.strip().split("\n")[1:]:
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 4:
|
||||
pub = parts[0]
|
||||
peer_keys.append(pub)
|
||||
try:
|
||||
transfer[pub] = [int(parts[5]), int(parts[6])]
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
handshakes[pub] = int(parts[4])
|
||||
except Exception:
|
||||
@@ -83,6 +88,7 @@ def health():
|
||||
peers = 0
|
||||
peer_keys = []
|
||||
handshakes = {}
|
||||
transfer = {}
|
||||
cpu = cpu_percent()
|
||||
try:
|
||||
parts = Path("/proc/meminfo").read_text().splitlines()
|
||||
@@ -96,7 +102,7 @@ def health():
|
||||
mem_str = f"{used}/{total}MB"
|
||||
except Exception:
|
||||
mem_str = "?"
|
||||
return jsonify({"status": "ok", "peers": peers, "peer_keys": peer_keys, "handshakes": handshakes, "cpu": cpu, "mem": mem_str, "net": net_bytes()})
|
||||
return jsonify({"status": "ok", "peers": peers, "peer_keys": peer_keys, "handshakes": handshakes, "transfer": transfer, "cpu": cpu, "mem": mem_str, "net": net_bytes()})
|
||||
|
||||
|
||||
@app.route("/api/obf-health")
|
||||
|
||||
@@ -96,8 +96,10 @@ FAIL_COUNT=$(cat "$STATE/fail_count" 2>/dev/null || echo "0")
|
||||
PRIMARY_CHECK_TS=$(cat "$STATE/primary_check_ts" 2>/dev/null || echo "0")
|
||||
|
||||
# ── Detect Phobos WG interface (non-fatal, fallback to Wireguard3) ──
|
||||
# Нет своего интерфейса — выходим: запасной Wireguard3 бывает чужим туннелем.
|
||||
detect_wg_interface || {
|
||||
log "WARN: detect failed, using default $WG_IF"
|
||||
log "ERROR: own WG interface not found, skip"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Get handshake age via ndmc ──
|
||||
|
||||
@@ -27,9 +27,16 @@ SERVER_WG_IPV4_NETWORK="${SERVER_WG_IPV4_NETWORK:-10.25.0.0/16}"
|
||||
SERVER_WG_IPV4_NETWORK="$(python3 - "$SERVER_WG_IPV4_NETWORK" <<'PY'
|
||||
import ipaddress, sys
|
||||
try:
|
||||
net = ipaddress.ip_network(sys.argv[1].strip(), strict=False)
|
||||
net = ipaddress.ip_network(sys.argv[1].strip(), strict=True)
|
||||
except ValueError:
|
||||
sys.exit("ERROR: bad subnet " + sys.argv[1])
|
||||
# «10.25.11.0/16» — биты хоста заданы: раньше молча превращалось в 10.25.0.0/16
|
||||
# (две установки с одинаковой подсетью → одинаковые IP клиентов, конфликт на роутере).
|
||||
# Считаем, что нужна подсеть этого адреса, и берём /24.
|
||||
try:
|
||||
net = ipaddress.ip_network(sys.argv[1].strip().split("/")[0] + "/24", strict=False)
|
||||
sys.stderr.write("NOTICE: subnet %s has host bits set, using %s\n" % (sys.argv[1], net))
|
||||
except ValueError:
|
||||
sys.exit("ERROR: bad subnet " + sys.argv[1])
|
||||
if net.version != 4 or not net.is_private or net.prefixlen > 29:
|
||||
sys.exit("ERROR: subnet must be private IPv4, /8../29 (e.g. 10.25.1.0/24)")
|
||||
print(net)
|
||||
|
||||
@@ -21,8 +21,10 @@ echo " watchdog, бинарники, $PHOBOS_DIR, $PANEL_DIR"
|
||||
echo "=================================================="
|
||||
if [ "${PURGE:-0}" != "1" ]; then
|
||||
printf "Точно удалить ВСЁ? [yes/NO]: "
|
||||
read ans
|
||||
[ "$ans" = "yes" ] || { echo "Отменено."; exit 0; }
|
||||
read -r ans </dev/tty 2>/dev/null || read -r ans
|
||||
# tolerate CR from Windows/phone terminals, spaces, case, "да"
|
||||
ans=$(printf '%s' "$ans" | tr -d '\r[:space:]' | tr '[:upper:]' '[:lower:]')
|
||||
case "$ans" in yes|y|да|д) ;; *) echo "Отменено (нужно ввести yes)."; exit 0 ;; esac
|
||||
fi
|
||||
|
||||
IFACE=$(ip route get 8.8.8.8 2>/dev/null | awk '{for(i=1;i<NF;i++) if($i=="dev") print $(i+1)}' | head -1)
|
||||
|
||||
Reference in New Issue
Block a user