release 1.4.11: update banner (new panel/app versions), GitHub adleibzon-rgb as primary source
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
3
APPS
Normal file
3
APPS
Normal file
@@ -0,0 +1,3 @@
|
||||
Android (PCA Phobos VPN)=1.4.6
|
||||
iPhone (TestFlight)=1.4.5
|
||||
Windows=1.4.5-beta
|
||||
@@ -1,3 +1,6 @@
|
||||
## 1.4.11 — 2026-11-11
|
||||
- Панель сама раз в час проверяет свой канал (stable/beta/dev): если вышла новая версия — сверху зелёная полоса «Доступна новая версия» и кнопка «Обновить». В окне перед обновлением — что нового, как сделать бэкап и как откатиться.
|
||||
|
||||
## 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`.
|
||||
|
||||
@@ -109,6 +109,7 @@ bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/install
|
||||
После установки панель напечатает адрес, логин, пароль и **API key** — сохрани их.
|
||||
|
||||
Полный список портов и что можно закрывать: [docs/PORTS.md](docs/PORTS.md).
|
||||
DNS клиентов и свой AdGuard Home: [docs/DNS.md](docs/DNS.md). WARP — проверка и проблемы: [docs/WARP.md](docs/WARP.md).
|
||||
|
||||
### Вторичный сервер (для failover / балансировки)
|
||||
|
||||
|
||||
292
app.py
292
app.py
@@ -36,7 +36,7 @@ SECRET_FILE = f"{PANEL_DIR}/.secret_key"
|
||||
WG_CONFIG = "/etc/wireguard/wg0.conf"
|
||||
# Distribution: Gitea first (GitHub account is unavailable since 23.10.2026), GitHub fallback
|
||||
PCA_GIT = "https://git.bobyrev.com/PCA/PCA_Phobos"
|
||||
PCA_GH_RAW = "https://raw.githubusercontent.com/andrey271192/PCA_Phobos"
|
||||
PCA_GH_RAW = "https://raw.githubusercontent.com/adleibzon-rgb/PCA_Phobos"
|
||||
APK_TAG = "v1.4.4"
|
||||
WARP_DIR = f"{PHOBOS_DIR}/server/warp"
|
||||
WARP_CUSTOM_PROFILE_FILE = f"{WARP_DIR}/custom-profile.conf"
|
||||
@@ -1071,15 +1071,15 @@ UPDATE_BIN = "/usr/local/bin/phobos-update"
|
||||
def _update_repo_for_ref(ref):
|
||||
ref = (ref or "").strip()
|
||||
if ref in ("beta", "dev"):
|
||||
return "andrey271192/PCA_Phobos-dev", ref
|
||||
return "adleibzon-rgb/PCA_Phobos-dev", ref
|
||||
if ref in ("stable", "main", "") or ref.startswith("v"):
|
||||
return "andrey271192/PCA_Phobos", "main" if ref in ("stable", "") else ref
|
||||
return "adleibzon-rgb/PCA_Phobos", "main" if ref in ("stable", "") else ref
|
||||
version = panel_version()
|
||||
if "beta" in version:
|
||||
return "andrey271192/PCA_Phobos-dev", "beta"
|
||||
return "adleibzon-rgb/PCA_Phobos-dev", "beta"
|
||||
if "dev" in version:
|
||||
return "andrey271192/PCA_Phobos-dev", "dev"
|
||||
return "andrey271192/PCA_Phobos", "main"
|
||||
return "adleibzon-rgb/PCA_Phobos-dev", "dev"
|
||||
return "adleibzon-rgb/PCA_Phobos", "main"
|
||||
|
||||
|
||||
def _current_update_ref(arg):
|
||||
@@ -1198,6 +1198,34 @@ def client_endpoint(ip):
|
||||
|
||||
|
||||
_HOST_RE = re.compile(r"^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$")
|
||||
DEFAULT_CLIENT_DNS = "1.1.1.1, 8.8.8.8"
|
||||
|
||||
|
||||
def client_dns():
|
||||
return _load_server_env().get("CLIENT_DNS", "").strip() or DEFAULT_CLIENT_DNS
|
||||
|
||||
|
||||
def _apply_client_dns(wg):
|
||||
"""Issued .conf files keep the DNS they were born with; serve the current one."""
|
||||
return re.sub(r"(?mi)^DNS\s*=.*$", "DNS = " + client_dns(), wg)
|
||||
|
||||
|
||||
def save_client_dns(form):
|
||||
raw = (form.get("client_dns", "") or "").replace(";", ",")
|
||||
items = [x.strip() for x in raw.split(",") if x.strip()]
|
||||
for x in items:
|
||||
try:
|
||||
ipaddress.ip_address(x)
|
||||
except ValueError:
|
||||
return False, tr("Неверный DNS: ", "Bad DNS: ") + x
|
||||
try:
|
||||
lines = [ln for ln in open(SERVER_ENV).read().splitlines() if not ln.startswith("CLIENT_DNS=")]
|
||||
if items and ", ".join(items) != DEFAULT_CLIENT_DNS:
|
||||
lines.append("CLIENT_DNS=" + ",".join(items)) # no spaces: server.env is also `source`d
|
||||
Path(SERVER_ENV).write_text("\n".join(lines) + "\n")
|
||||
except Exception as e:
|
||||
return False, f"server.env: {e}"
|
||||
return True, "DNS = " + client_dns()
|
||||
|
||||
|
||||
def save_client_endpoints(form):
|
||||
@@ -1251,6 +1279,93 @@ def run_speedtest(ip):
|
||||
return res
|
||||
|
||||
|
||||
|
||||
# --- Google geo (09.30): how Google sees the server's addresses ---------------
|
||||
GOOGLE_CLOSED = {"Russia", "Belarus", "China", "Iran", "Hong Kong", "North Korea", "Cuba", "Syria"}
|
||||
|
||||
|
||||
def _google_country(*args):
|
||||
"""«Country version» at the bottom of policies.google.com/terms = the country Google assigns to the address."""
|
||||
import re as _re
|
||||
import subprocess as _sp
|
||||
try:
|
||||
html = _sp.run(["curl", *args, "-s", "-m", "15", "https://policies.google.com/terms?hl=en"],
|
||||
capture_output=True, text=True, timeout=25).stdout
|
||||
m = _re.search(r"Country version:\s*</a>\s*([A-Za-z][A-Za-z .'-]*?)\s*<", html)
|
||||
return m.group(1) if m else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def google_geo():
|
||||
"""v4/v6 = default route (what clients exit with); ips = every public IPv4 on the server."""
|
||||
import re as _re
|
||||
import subprocess as _sp
|
||||
import time as _t
|
||||
out = {"v4": _google_country("-4"), "v6": _google_country("-6"), "ips": {}, "ts": int(_t.time())}
|
||||
try:
|
||||
for line in _sp.run(["ip", "-4", "-o", "addr", "show", "scope", "global"], capture_output=True,
|
||||
text=True, timeout=10).stdout.splitlines():
|
||||
ip = line.split()[3].split("/")[0]
|
||||
if not _re.match(r"^(10|127|172\.(1[6-9]|2\d|3[01])|192\.168|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7]))\.", ip):
|
||||
out["ips"][ip] = _google_country("-4", "--interface", ip)
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
_google_geo_busy = set()
|
||||
|
||||
|
||||
def refresh_google_geo(ip):
|
||||
"""Background refresh of google_geo for one server (main: locally, secondary: mini-API)."""
|
||||
if ip in _google_geo_busy:
|
||||
return
|
||||
_google_geo_busy.add(ip)
|
||||
|
||||
def _run():
|
||||
try:
|
||||
if not ip or ip == SERVER_IP:
|
||||
res = google_geo()
|
||||
else:
|
||||
srv = next((x for x in load_servers() if x.get("ip") == ip), None)
|
||||
res = secondary_api_request(srv, "/api/google-geo", None, timeout=120) if srv else None
|
||||
if res and "v4" in res:
|
||||
s = load_settings()
|
||||
s.setdefault("google_geo", {})[ip or SERVER_IP] = res
|
||||
save_settings(s)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
_google_geo_busy.discard(ip)
|
||||
import threading as _th
|
||||
_th.Thread(target=_run, daemon=True).start()
|
||||
|
||||
|
||||
def google_geo_html(ip):
|
||||
"""Card lines: «Google видит: IPv4 X · IPv6 Y» (+ other IPv4), red + warning if Google marks it closed."""
|
||||
import time as _t
|
||||
g = load_settings().get("google_geo", {}).get(ip) or {}
|
||||
if not g or _t.time() - g.get("ts", 0) > 3 * 3600:
|
||||
refresh_google_geo(ip)
|
||||
if not g:
|
||||
return f'{tr("Google видит", "Google sees")}: <span>{tr("проверяется…", "checking…")}</span>'
|
||||
def b(v):
|
||||
col = "#e66" if v in GOOGLE_CLOSED else "var(--text)"
|
||||
return f'<b style="color:{col}">{v or "—"}</b>'
|
||||
others = " · ".join(f'{k} {b(v)}' for k, v in sorted((g.get("ips") or {}).items()) if k != ip)
|
||||
ago = int((_t.time() - g.get("ts", 0)) / 60)
|
||||
html = (f'{tr("Google видит", "Google sees")}: IPv4 {b(g.get("v4"))} · IPv6 {b(g.get("v6"))} '
|
||||
f'<span style="opacity:.7">({ago} {tr("мин назад", "min ago")})</span>')
|
||||
if others:
|
||||
html += f'<br><span style="opacity:.8">{tr("другие IPv4", "other IPv4")}: {others}</span>'
|
||||
bad = [v for v in (g.get("v4"), g.get("v6")) if v in GOOGLE_CLOSED]
|
||||
if bad:
|
||||
html += (f'<br><span style="color:#e66">{tr("Google считает адрес", "Google treats the address as")} «{", ".join(bad)}» — '
|
||||
f'{tr("Gemini и часть сервисов Google у клиентов не откроются", "Gemini and some Google services will not open for clients")}</span>')
|
||||
return html
|
||||
|
||||
|
||||
LIMITS_FILE = f"{PHOBOS_DIR}/server/limits.conf"
|
||||
|
||||
|
||||
@@ -1402,7 +1517,7 @@ def build_phone_config(cid, mode="android"):
|
||||
instpath = os.path.join(cdir, "wg-obfuscator.conf")
|
||||
if not os.path.exists(wgpath):
|
||||
return None, None
|
||||
wg = open(wgpath).read().rstrip()
|
||||
wg = _apply_client_dns(open(wgpath).read().rstrip())
|
||||
if mode == "ios":
|
||||
server_ip = SERVER_IP
|
||||
try:
|
||||
@@ -1436,7 +1551,8 @@ def build_phone_config(cid, mode="android"):
|
||||
inst += "\n# pca-tcp = " + tcp_port
|
||||
conf = wg + "\n\n" + inst + "\n"
|
||||
payload = _prepare_phoboswg_payload(conf)
|
||||
b64 = base64.urlsafe_b64encode(payload.encode()).decode()
|
||||
# spec 2.1: base64url WITHOUT padding (HydraRoute rejects a trailing "=")
|
||||
b64 = base64.urlsafe_b64encode(payload.encode()).decode().rstrip("=")
|
||||
link = "phobos://" + b64 + "#" + urllib.parse.quote(cid or "none")
|
||||
return payload, link
|
||||
|
||||
@@ -1916,7 +2032,7 @@ textarea::placeholder{color:var(--muted)}
|
||||
</div>
|
||||
CONTENT
|
||||
<div class="footer">
|
||||
<b style="color:var(--accent)">PCA Phobos Panel</b> — панель управления © <a href="https://git.bobyrev.com/PCA/PCA_Phobos" target="_blank" rel="noopener">andrey271192</a><br>
|
||||
<b style="color:var(--accent)">PCA Phobos Panel</b> — панель управления © <a href="https://git.bobyrev.com/PCA/PCA_Phobos" target="_blank" rel="noopener">adleibzon-rgb</a><br>
|
||||
Поддержать проект: <a href="https://boosty.to/andrey27/donate" target="_blank" rel="noopener">💖 Boosty</a> · <a href="https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798" target="_blank" rel="noopener">💳 Ozon Bank</a> · <a href="https://t.me/PCAdministration" target="_blank" rel="noopener">✉️ Telegram</a>
|
||||
<div style="margin-top:8px;font-size:.88em;color:var(--muted);line-height:1.7;text-align:left;max-width:660px;margin:8px auto 0">
|
||||
<b>Создано на основе открытых проектов:</b><br>
|
||||
@@ -1931,8 +2047,141 @@ function showHelp(el){var b=el.nextElementSibling; if(b&&b.classList.contains('h
|
||||
</body></html>"""
|
||||
|
||||
|
||||
# ── «Доступна новая версия»: проверка канала раз в час + баннер с описанием ──
|
||||
_upd_cache = {"ts": 0, "info": None, "busy": False}
|
||||
|
||||
|
||||
def _channel_of(version):
|
||||
v = (version or "").lower()
|
||||
return "dev" if "dev" in v else ("beta" if "beta" in v else "stable")
|
||||
|
||||
|
||||
def _ver_key(v):
|
||||
"""1.4.10 > 1.4.9; 1.5.0-beta.2 > 1.5.0-beta.1; release > pre-release of the same number."""
|
||||
v = (v or "").strip().lstrip("v")
|
||||
base, _, pre = v.partition("-")
|
||||
nums = [int(x) if x.isdigit() else 0 for x in base.split(".")[:3]]
|
||||
nums += [0] * (3 - len(nums))
|
||||
pre_n = [int(x) for x in re.findall(r"\d+", pre)] or [0]
|
||||
return (nums, 0 if pre else 1, pre_n)
|
||||
|
||||
|
||||
def _fetch_channel_file(channel, name):
|
||||
repo = "PCA_Phobos" if channel == "stable" else "PCA_Phobos-dev"
|
||||
branch = {"stable": "main"}.get(channel, channel)
|
||||
cmd = ["curl", "-fsSL", "-m", "10", f"https://git.bobyrev.com/PCA/{repo}/raw/branch/{branch}/{name}?cb={int(time.time())}"]
|
||||
try:
|
||||
tok = Path(GH_TOKEN_FILE).read_text().strip()
|
||||
if tok and repo != "PCA_Phobos":
|
||||
cmd[1:1] = ["-H", "Authorization: token " + tok]
|
||||
except Exception:
|
||||
pass
|
||||
if channel == "stable": # public: GitHub first, Gitea fallback (09.33)
|
||||
g = subprocess.run(["curl", "-fsSL", "-m", "10", f"https://raw.githubusercontent.com/adleibzon-rgb/PCA_Phobos/main/{name}?cb={int(time.time())}"],
|
||||
capture_output=True, text=True, timeout=15)
|
||||
if g.returncode == 0 and g.stdout:
|
||||
return g.stdout
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
||||
return r.stdout if r.returncode == 0 else ""
|
||||
|
||||
|
||||
def _changelog_since(text, cur):
|
||||
"""Sections '## X.Y.Z …' of CHANGELOG newer than the installed version."""
|
||||
out, take = [], False
|
||||
for ln in text.split("\n"):
|
||||
m = re.match(r"^## +v?(\d+\.\d+\.\d+\S*)", ln)
|
||||
if m:
|
||||
take = _ver_key(m.group(1)) > _ver_key(cur)
|
||||
elif ln.startswith("## "):
|
||||
take = False
|
||||
if take:
|
||||
out.append(ln)
|
||||
return "\n".join(out).strip()
|
||||
|
||||
|
||||
def _apps_parse(text):
|
||||
out = {}
|
||||
for ln in (text or "").splitlines():
|
||||
if "=" in ln and not ln.lstrip().startswith("#"):
|
||||
k, v = ln.split("=", 1)
|
||||
out[k.strip()] = v.strip()
|
||||
return out
|
||||
|
||||
|
||||
def _refresh_update_info():
|
||||
try:
|
||||
cur = panel_version()
|
||||
ch = _channel_of(cur)
|
||||
latest = _fetch_channel_file(ch, "VERSION").strip()
|
||||
info = None
|
||||
try:
|
||||
local_apps = _apps_parse(open(os.path.join(PANEL_DIR, "APPS")).read())
|
||||
except Exception:
|
||||
local_apps = {}
|
||||
remote_apps = _apps_parse(_fetch_channel_file(ch, "APPS"))
|
||||
apps = [(k, local_apps.get(k, ""), v) for k, v in remote_apps.items()
|
||||
if not local_apps.get(k) or _ver_key(v) > _ver_key(local_apps.get(k))]
|
||||
newer = bool(latest and re.match(r"^v?\d+\.\d+", latest) and _ver_key(latest) > _ver_key(cur))
|
||||
if newer or apps:
|
||||
info = {"channel": ch, "cur": cur, "latest": latest if newer else cur, "apps": apps,
|
||||
"notes": _changelog_since(_fetch_channel_file(ch, "CHANGELOG.md"), cur)[:6000] if newer else ""}
|
||||
_upd_cache["info"] = info
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
_upd_cache["ts"] = time.time()
|
||||
_upd_cache["busy"] = False
|
||||
|
||||
|
||||
def update_info():
|
||||
if not _upd_cache["busy"] and time.time() - _upd_cache["ts"] > 3600:
|
||||
_upd_cache["busy"] = True
|
||||
threading.Thread(target=_refresh_update_info, daemon=True).start()
|
||||
return _upd_cache["info"]
|
||||
|
||||
|
||||
def update_banner():
|
||||
try:
|
||||
if not session.get("auth"):
|
||||
return ""
|
||||
except Exception:
|
||||
return ""
|
||||
i = update_info()
|
||||
if not i:
|
||||
return ""
|
||||
e = html_lib.escape
|
||||
notes = e(i["notes"] or tr("Описание изменений — в CHANGELOG.md репозитория.", "See CHANGELOG.md in the repository."))
|
||||
apps = i.get("apps") or []
|
||||
apps_line = (" · " + tr("новые приложения", "new apps") + ": " + ", ".join(e(f"{k} {v}") for k, _o, v in apps)) if apps else ""
|
||||
apps_html = ("<p style=\"font-size:.88em\"><b>" + tr("Новые версии приложений", "New app versions") + ":</b><br>"
|
||||
+ "<br>".join(e(f"{k}: {o or '—'} → {v}") for k, o, v in apps)
|
||||
+ "<br>" + tr("После обновления панели новые ссылки на скачивание — в окне «Подключение» у клиента; разошлите их пользователям.", "After the panel update the new download links are in the client's Connection window; send them to your users.")
|
||||
+ "</p>") if apps else ""
|
||||
return f'''<div id="upd-bar" style="background:#16a34a;color:#fff;padding:8px 14px;text-align:center;font-size:.92em">
|
||||
{tr("Доступна новая версия","New version available")} <b>{e(i["latest"])}</b> ({e(i["channel"])}) · {tr("у вас","you have")} {e(i["cur"])}{apps_line}
|
||||
<button class="btn btn-sm" style="background:#fff;color:#16a34a;margin-left:10px" onclick="document.getElementById('upd-modal').style.display='flex'">{tr("Обновить","Update")}</button>
|
||||
</div>
|
||||
<div id="upd-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9999;align-items:center;justify-content:center;padding:16px">
|
||||
<div class="card" style="max-width:640px;width:100%;max-height:85vh;overflow:auto;margin:0">
|
||||
<h2>{tr("Обновление до","Update to")} {e(i["latest"])} ({e(i["channel"])})</h2>
|
||||
<p style="color:var(--muted);font-size:.88em">{tr("Что нового","What's new")}:</p>
|
||||
<pre style="white-space:pre-wrap;font-size:.85em;background:var(--bg);padding:10px;border-radius:8px">{notes}</pre>
|
||||
{apps_html}
|
||||
<p style="font-size:.88em"><b>{tr("Перед обновлением","Before updating")}:</b> {tr("панель сама делает бэкап PCA-слоя; ключи WireGuard, server.env и клиенты не трогаются. Свой полный бэкап (рекомендуем):","the panel backs up the PCA layer itself; WireGuard keys, server.env and clients are not touched. Your own full backup (recommended):")}<br>
|
||||
<code>tar czf /root/phobos-backup-$(date +%F).tgz /opt/Phobos /opt/phobos-panel</code></p>
|
||||
<p style="font-size:.88em"><b>{tr("Если что-то сломалось","If something breaks")}:</b> {tr("Настройки → «Откатить на предыдущую» или в консоли","Settings → Rollback, or in the console")} <code>phobos-update --rollback</code>.
|
||||
{tr("Обновлены secondary-серверы? Обновите и их той же кнопкой в их панели.","Have secondary servers? Update them the same way in their panel.")}</p>
|
||||
<form method="post" action="/settings" style="display:flex;gap:8px;justify-content:flex-end;margin-top:10px">
|
||||
<input type="hidden" name="action" value="update"><input type="hidden" name="target" value="{e(i["channel"])}">
|
||||
<button type="button" class="btn btn-sm" style="background:#475569" onclick="document.getElementById('upd-modal').style.display='none'">{tr("Отмена","Cancel")}</button>
|
||||
<button type="submit" class="btn btn-primary">{tr("Обновить сейчас","Update now")}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def render(content):
|
||||
return PAGE.replace("CONTENT", content)
|
||||
return PAGE.replace("CONTENT", update_banner() + content)
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
@@ -2805,6 +3054,7 @@ def settings_page():
|
||||
_out = tr("WARP настройки сохранены.", "WARP settings saved.")
|
||||
elif _act == "update":
|
||||
_out = run_update(request.form.get("target", "").strip() or "main")
|
||||
_upd_cache["ts"] = 0
|
||||
elif _act == "check":
|
||||
_out = run_update("--check")
|
||||
elif _act == "warp_enable":
|
||||
@@ -2842,6 +3092,8 @@ def settings_page():
|
||||
ok, _out = run_server_script("phobos-domain.sh", "remove")
|
||||
elif _act == "endpoints_save":
|
||||
ok, _out = save_client_endpoints(request.form)
|
||||
elif _act == "dns_save":
|
||||
ok, _out = save_client_dns(request.form)
|
||||
else:
|
||||
ok, _out = run_server_script("phobos-guard.sh", _act.split("_", 1)[1], timeout=300)
|
||||
cls = "alert-ok" if ok else "alert-error"
|
||||
@@ -2950,6 +3202,15 @@ def settings_page():
|
||||
<p style="font-size:.8em;color:var(--muted);margin-top:6px">{tr("Отключить: phobos-cascade disable · статус: phobos-cascade status. iOS/обычный WireGuard (порт 51820) через каскад не идёт.","Disable: phobos-cascade disable · status: phobos-cascade status. iOS/plain WireGuard (port 51820) does not go through the cascade.")}</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>{tr("DNS для клиентов","Client DNS")} {hlp("DNS-серверы в конфиге клиента (строка DNS = в [Interface]). Пусто = 1.1.1.1, 8.8.8.8. Новые клиенты получают сразу; уже выданным — заново открыть ссылку/QR. Роутеры Keenetic раздают в сеть свой DNS. Если у клиента включён firewall (🛡) — добавьте эти адреса в разрешённые.", "DNS servers in the client config (DNS = line in [Interface]). Empty = 1.1.1.1, 8.8.8.8. New clients get it at once; existing ones must reopen the link/QR. Keenetic routers hand out their own DNS to the LAN. If a client has the firewall (🛡) on, add these addresses to its allow list.")}</h2>
|
||||
<form method="post" class="form-row">
|
||||
<input type="hidden" name="action" value="dns_save">
|
||||
<input name="client_dns" value="{_html_attr(client_dns())}" placeholder="1.1.1.1, 8.8.8.8" style="width:260px;padding:5px 8px">
|
||||
<button class="btn btn-primary btn-sm" type="submit">{tr("Сохранить","Save")}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>{tr("Защита сервера (сторож)","Server protection (guard)")} {hlp("Блокирует подсети зондов и сканеров РКН до того, как они дойдут до сервера (nftables). Список IP — из проекта Loorrr293/Auto_IPtables, обновляется раз в сутки. Частные сети (LAN, туннель) никогда не блокируются.", "Drops RKN probe/scanner subnets before they reach the server (nftables). IP list from Loorrr293/Auto_IPtables, refreshed daily. Private networks (LAN, tunnel) are never blocked.")}</h2>
|
||||
<p>{tr("Статус","Status")}: <b style="color:{'#16a34a' if guard.get("enabled") else '#f59e0b'}">{tr("включён","enabled") if guard.get("enabled") else tr("выключен","disabled")}</b>
|
||||
@@ -3080,6 +3341,7 @@ def settings_page():
|
||||
<tr><td>{tr("WARP+ key","WARP+ key")}</td><td>{tr("сохранён","saved") if warp_license_saved else tr("не задан","not set")}</td></tr>
|
||||
<tr><td>{tr("Custom profile","Custom profile")}</td><td>{tr("сохранён","saved") if custom_profile_saved else tr("не задан","not set")}</td></tr>
|
||||
<tr><td>{tr("Egress IPv4","Egress IPv4")}</td><td>{warp_egress or '—'}</td></tr>
|
||||
<tr><td>Handshake</td><td>{(str(warp.get("handshake_age")) + tr(" с назад", "s ago")) if warp.get("handshake_age", -1) >= 0 else tr("нет — UDP до Cloudflare не проходит", "none — UDP to Cloudflare blocked")}</td></tr>
|
||||
</table>
|
||||
<form method="post" style="margin-top:10px">
|
||||
<input type="hidden" name="action" value="warp_save">
|
||||
@@ -3465,13 +3727,12 @@ def download_apk():
|
||||
@auth_required
|
||||
def client_conf_download(cid, mode):
|
||||
m = "ios" if mode == "ios" else "android"
|
||||
if m == "android":
|
||||
return ("Android/PhobosWG uses phobos:// QR/link import; .conf import is only valid for iOS/plain WireGuard.", 400)
|
||||
conf, _link = build_phone_config(cid, m)
|
||||
if conf is None:
|
||||
return ("client not found", 404)
|
||||
if m == "android":
|
||||
conf = _pad_conf_with_none(conf)
|
||||
# raw .conf carries no "= none" placeholders (spec 2.3: padding is for the link only)
|
||||
conf = "\n".join(ln for ln in conf.split("\n") if ln.partition("=")[2].strip().lower() != "none")
|
||||
return (conf, 200, {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Content-Disposition": 'attachment; filename="%s-%s.conf"' % (cid, m),
|
||||
@@ -3512,7 +3773,9 @@ def client_phone(cid, mode):
|
||||
f'<p style="font-size:.8em;color:var(--muted)">{tr("Поставь любое из двух приложений и открой ссылку ниже на телефоне — профиль импортируется сам.","Install either app and open the link below on the phone — the profile imports itself.")}</p>')
|
||||
extra = (client_btn + apk_btn +
|
||||
f'<p>{tr("phobos:// ссылка — импорт в PCA Phobos VPN или PhobosWG:", "phobos:// link — import into PCA Phobos VPN or PhobosWG:")} <a href="{link}">{tr("открыть", "open")}</a></p>'
|
||||
f'<textarea readonly onclick="this.select()" style="width:100%;height:90px;font-size:.75em">{link}</textarea>')
|
||||
f'<textarea readonly onclick="this.select()" style="width:100%;height:90px;font-size:.75em">{link}</textarea>'
|
||||
f'<p style="font-size:.85em"><a href="/client/{cid}/conf/android">⬇ {tr("Файл .conf — для «Импорт из файла» в PCA Phobos VPN", ".conf file — for “Import from file” in PCA Phobos VPN")}</a>. '
|
||||
f'{tr("Ссылка выше подходит и для HydraRoute (формат phobos:// автора Phobos).", "The link above also works in HydraRoute (upstream phobos:// format).")}</p>')
|
||||
else:
|
||||
title = tr("iPhone / iOS WireGuard (без обфускации)", "iPhone / iOS WireGuard (plain, no obfuscation)")
|
||||
extra = (f'<p><a href="https://apps.apple.com/app/wireguard/id1441195209" target="_blank" rel="noopener" class="btn btn-primary" style="background:#0ea5e9">📥 {tr("WireGuard в App Store","WireGuard on the App Store")}</a> <a href="/client/{cid}/conf/ios" class="btn" style="background:#7c3aed">⬇ {tr("Скачать .conf","Download .conf")}</a></p>'
|
||||
@@ -4478,6 +4741,7 @@ def servers_page():
|
||||
Peers: <b style="color:var(--text)">{peers_val}</b> Assigned: <b style="color:var(--text)">{assigned}</b> Ports: {ports}<br>
|
||||
<span title="Канал / Channel (rx+tx)">↕ <b style="color:var(--text)">{mbps_disp}</b></span><br>
|
||||
{speed_line}
|
||||
<br>{google_geo_html(ip)}
|
||||
</div>
|
||||
<form method="post" style="margin-top:8px" onsubmit="this.querySelector('button').textContent='⏳ ~20 s…'">
|
||||
<input type="hidden" name="action" value="speedtest"><input type="hidden" name="ip" value="{ip}">
|
||||
|
||||
51
docs/DNS.md
Normal file
51
docs/DNS.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# DNS для клиентов и свой AdGuard Home
|
||||
|
||||
## Какие DNS получают клиенты
|
||||
|
||||
По умолчанию в конфиге клиента (секция `[Interface]`) стоит строка
|
||||
|
||||
```
|
||||
DNS = 1.1.1.1, 8.8.8.8
|
||||
```
|
||||
|
||||
То есть Cloudflare и Google.
|
||||
|
||||
## Где посмотреть и поменять
|
||||
|
||||
**В панели:** «Настройки» → «DNS для клиентов». Впишите адреса через запятую. Если поле пустое, используются 1.1.1.1 и 8.8.8.8.
|
||||
|
||||
- Новые клиенты получают новый DNS сразу.
|
||||
- Уже выданным клиентам достаточно заново открыть ссылку или QR, пересоздавать клиента не нужно.
|
||||
- Значение хранится в `/opt/Phobos/server/server.env` в строке `CLIENT_DNS=...`, без пробелов.
|
||||
|
||||
**Вручную, только для одного клиента:** исправьте строку `DNS =` в его `.conf` и заново импортируйте конфиг.
|
||||
|
||||
**Роутеры Keenetic** раздают устройствам в домашней сети свой собственный DNS, строка из конфига на них не влияет.
|
||||
|
||||
**Firewall клиента (🛡).** Если клиенту задан список разрешённых адресов, всё остальное блокируется, и запросы к DNS тоже. Добавьте в разрешённые адреса DNS-серверов, например `1.1.1.1:53/udp, 8.8.8.8:53/udp`.
|
||||
|
||||
## Свой AdGuard Home на том же VPS
|
||||
|
||||
> `127.0.0.1` в конфиг клиента прописывать **нельзя**. Строку `DNS =` выполняет устройство клиента, и `127.0.0.1` для него — это оно само, а не сервер.
|
||||
|
||||
Указывать нужно адрес сервера **внутри туннеля**. Это первый адрес подсети WG, его видно в строке `Address` файла `/etc/wireguard/wg0.conf` на сервере, например `10.25.11.1`. Закрытый наружу 53 порт ничему не мешает: запросы идут внутри туннеля через интерфейс `wg0`.
|
||||
|
||||
1. **AGH должен слушать адрес wg0.** В `AdGuardHome.yaml`, в разделе `dns.bind_hosts`, должен быть `10.25.11.1` или `0.0.0.0`. Если там только `127.0.0.1`, добавьте адрес wg0 и перезапустите AGH.
|
||||
2. **Откройте 53 порт только из туннеля** (нужно, если входящие соединения на сервере закрыты):
|
||||
```bash
|
||||
iptables -I INPUT -i wg0 -p udp --dport 53 -j ACCEPT
|
||||
iptables -I INPUT -i wg0 -p tcp --dport 53 -j ACCEPT
|
||||
```
|
||||
Чтобы правило пережило перезагрузку, сохраните его, например через `netfilter-persistent save`.
|
||||
3. **Пропишите этот адрес клиентам:** панель → «Настройки» → «DNS для клиентов» → `10.25.11.1`.
|
||||
4. Если у клиента включён firewall 🛡, добавьте в разрешённые `10.25.11.1:53/udp`.
|
||||
|
||||
Проверка с устройства клиента при включённом VPN:
|
||||
|
||||
```bash
|
||||
nslookup ya.ru 10.25.11.1
|
||||
```
|
||||
|
||||
Запрос должен появиться в журнале запросов AGH.
|
||||
|
||||
**Если серверов несколько.** Клиент может переключиться на другой сервер, а адрес DNS в конфиге один для всех. Поэтому либо ставьте AGH на каждый сервер так, чтобы он слушал адрес wg0 этого сервера, либо оставьте общедоступные DNS.
|
||||
29
docs/WARP.md
Normal file
29
docs/WARP.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# WARP: проверка и типичные проблемы
|
||||
|
||||
WARP включается в панели: «Настройки» → WARP. Режим egress выпускает исходящий трафик клиентов в интернет через Cloudflare: всех клиентов или только отмеченных.
|
||||
|
||||
## Как понять, что WARP реально работает
|
||||
|
||||
В карточке WARP есть строка **Handshake**:
|
||||
|
||||
- **«N с назад»** — связь с Cloudflare есть.
|
||||
- **«нет — UDP до Cloudflare не проходит»** — интерфейс поднят, но Cloudflare не отвечает. Трафик клиентов через WARP идти не будет.
|
||||
|
||||
Проверка с сервера:
|
||||
|
||||
```bash
|
||||
wg show warp latest-handshakes
|
||||
curl --interface warp -m 6 https://www.cloudflare.com/cdn-cgi/trace | grep warp= # ожидается warp=on
|
||||
phobos-warp status
|
||||
```
|
||||
|
||||
## Что делает панель сама
|
||||
|
||||
- **Ищет рабочий адрес Cloudflare.** При включении перебирает адреса и порты (2408 / 500 / 1701 / 4500) и записывает в конфиг первый, с которым появился handshake. Если не подошёл ни один, это занимает около минуты.
|
||||
- **Исправляет размер пакетов.** У WARP MTU 1280, у клиентов 1420. Без поправки крупные TCP-пакеты терялись, и интернет у клиентов работал «через раз». Теперь сервер автоматически уменьшает размер TCP-пакетов под WARP (правило MSS-clamp).
|
||||
|
||||
## «У клиентов нет интернета», IP Cloudflare показывается через раз
|
||||
|
||||
1. Обновите панель до версии с этими исправлениями («Версия и обновления»), затем выключите и снова включите WARP.
|
||||
2. Посмотрите строку Handshake. Если там «нет», значит UDP до Cloudflare с этого VPS режется. Обычно так бывает на VPS в РФ, где может не проходить даже регистрация WARP. В этом случае держите WARP выключенным или используйте его на сервере за рубежом.
|
||||
3. Если включён firewall клиента (🛡), проверьте, что в разрешённых есть DNS-серверы. См. [DNS.md](DNS.md).
|
||||
@@ -68,7 +68,7 @@ PY
|
||||
# Источник: Gitea git.bobyrev.com (основной), GitHub — запасной.
|
||||
# Каналы stable/beta/dev — ветки main/beta/dev одного публичного репозитория.
|
||||
PCA_GIT="${PCA_GIT:-https://git.bobyrev.com/PCA/PCA_Phobos}"
|
||||
GH_RAW="${GH_RAW:-https://raw.githubusercontent.com/andrey271192/PCA_Phobos}"
|
||||
GH_RAW="${GH_RAW:-https://raw.githubusercontent.com/adleibzon-rgb/PCA_Phobos}"
|
||||
CHANNEL="${CHANNEL:-stable}"
|
||||
case "$CHANNEL" in
|
||||
stable|main|"") PCA_BRANCH="${PCA_BRANCH:-main}";;
|
||||
|
||||
@@ -108,7 +108,7 @@ PY
|
||||
[Interface]
|
||||
PrivateKey = $priv_key
|
||||
Address = $addr_str
|
||||
DNS = 1.1.1.1, 8.8.8.8
|
||||
DNS = ${CLIENT_DNS:-1.1.1.1, 8.8.8.8}
|
||||
MTU = 1420
|
||||
|
||||
[Peer]
|
||||
|
||||
10
promote.sh
10
promote.sh
@@ -3,9 +3,9 @@
|
||||
# PCA Phobos — promote / sync каналов (maintainer-инструмент)
|
||||
#
|
||||
# Каналы и репозитории:
|
||||
# stable -> публичный andrey271192/PCA_Phobos ветка main
|
||||
# beta -> приватный andrey271192/PCA_Phobos-dev ветка beta
|
||||
# dev -> приватный andrey271192/PCA_Phobos-dev ветка dev
|
||||
# stable -> публичный adleibzon-rgb/PCA_Phobos ветка main
|
||||
# beta -> приватный adleibzon-rgb/PCA_Phobos-dev ветка beta
|
||||
# dev -> приватный adleibzon-rgb/PCA_Phobos-dev ветка dev
|
||||
#
|
||||
# Поток фич — строго в одну сторону: dev -> beta -> stable
|
||||
# Хотфикс в stable -> forward-port в beta и dev (чтоб не потерялся).
|
||||
@@ -22,8 +22,8 @@
|
||||
set -e
|
||||
GH_TOKEN="${GH_TOKEN:-}"
|
||||
[ -n "$GH_TOKEN" ] || { echo "Нужен GH_TOKEN (env)."; exit 1; }
|
||||
PUB="https://andrey271192:${GH_TOKEN}@github.com/andrey271192/PCA_Phobos.git"
|
||||
PRIV="https://andrey271192:${GH_TOKEN}@github.com/andrey271192/PCA_Phobos-dev.git"
|
||||
PUB="https://adleibzon-rgb:${GH_TOKEN}@github.com/adleibzon-rgb/PCA_Phobos.git"
|
||||
PRIV="https://adleibzon-rgb:${GH_TOKEN}@github.com/adleibzon-rgb/PCA_Phobos-dev.git"
|
||||
WORK="${PCA_WORK:-/tmp/pca-promote-work}"
|
||||
|
||||
repo_for() { case "$1" in main) echo "$PUB";; beta|dev) echo "$PRIV";; esac; }
|
||||
|
||||
@@ -194,6 +194,8 @@ post_up = (
|
||||
"sysctl -w net.ipv4.ip_forward=1 >/dev/null; "
|
||||
f"ip route replace {subnet} dev wg0 table {table}; "
|
||||
f"ip route replace default dev %i table {table}; "
|
||||
"iptables -t mangle -C FORWARD -o %i -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu 2>/dev/null || "
|
||||
"iptables -t mangle -A FORWARD -o %i -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu; "
|
||||
"iptables -C FORWARD -i wg0 -o %i -j ACCEPT 2>/dev/null || iptables -I FORWARD 1 -i wg0 -o %i -j ACCEPT; "
|
||||
"iptables -C FORWARD -i %i -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || "
|
||||
"iptables -I FORWARD 1 -i %i -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; "
|
||||
@@ -203,6 +205,7 @@ post_up = (
|
||||
post_down = (
|
||||
f"ip route del {subnet} dev wg0 table {table} 2>/dev/null || true; "
|
||||
f"ip route del default dev %i table {table} 2>/dev/null || true; "
|
||||
"iptables -t mangle -D FORWARD -o %i -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu 2>/dev/null || true; "
|
||||
"iptables -D FORWARD -i wg0 -o %i -j ACCEPT 2>/dev/null || true; "
|
||||
"iptables -D FORWARD -i %i -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true; "
|
||||
f"iptables -t nat -D POSTROUTING -s {subnet} -o %i -j MASQUERADE 2>/dev/null || true"
|
||||
@@ -234,6 +237,7 @@ clear_egress() {
|
||||
systemctl disable --now "wg-quick@${WARP_IF}" >/dev/null 2>&1 || true
|
||||
ip route del "$(wg_subnet)" dev wg0 table "$WARP_TABLE" 2>/dev/null || true
|
||||
ip route del default dev "$WARP_IF" table "$WARP_TABLE" 2>/dev/null || true
|
||||
iptables -t mangle -D FORWARD -o "$WARP_IF" -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu 2>/dev/null || true
|
||||
iptables -D FORWARD -i wg0 -o "$WARP_IF" -j ACCEPT 2>/dev/null || true
|
||||
iptables -D FORWARD -i "$WARP_IF" -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
|
||||
iptables -t nat -D POSTROUTING -s "$(wg_subnet)" -o "$WARP_IF" -j MASQUERADE 2>/dev/null || true
|
||||
@@ -356,6 +360,35 @@ relay_enabled() {
|
||||
iptables-save 2>/dev/null | grep -q "$RELAY_TAG_PREFIX"
|
||||
}
|
||||
|
||||
warp_handshake_age() {
|
||||
local ts
|
||||
ts="$(wg show "$WARP_IF" latest-handshakes 2>/dev/null | awk 'NR==1{print $2}')"
|
||||
[ -n "$ts" ] && [ "$ts" != "0" ] && echo $(( $(date +%s) - ts )) || echo -1
|
||||
}
|
||||
|
||||
# Cloudflare UDP to engage.cloudflareclient.com:2408 is often cut on the way:
|
||||
# try the profile endpoint first, then known WARP IP:port pairs, keep the first with a handshake.
|
||||
pick_warp_endpoint() {
|
||||
local pub ep cur i age
|
||||
pub="$(wg show "$WARP_IF" peers 2>/dev/null | head -1)"
|
||||
[ -n "$pub" ] || return 0
|
||||
cur="$(awk -F'= *' '/^Endpoint *=/{print $2; exit}' "$WARP_CONF" | tr -d ' ')"
|
||||
for ep in "$cur" 162.159.192.1:2408 162.159.193.1:2408 162.159.192.1:500 162.159.192.1:1701 162.159.192.1:4500 162.159.195.1:2408 188.114.97.1:2408; do
|
||||
[ -n "$ep" ] || continue
|
||||
wg set "$WARP_IF" peer "$pub" endpoint "$ep" 2>/dev/null || continue
|
||||
for i in 1 2 3 4 5 6; do
|
||||
ping -c1 -W1 -I "$WARP_IF" 1.1.1.1 >/dev/null 2>&1 || true
|
||||
age="$(warp_handshake_age)"
|
||||
if [ "$age" -ge 0 ] && [ "$age" -lt 30 ]; then
|
||||
[ "$ep" = "$cur" ] || sed -i "s|^Endpoint *=.*|Endpoint = ${ep}|" "$WARP_CONF"
|
||||
echo "WARP endpoint: $ep" >&2
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
echo "WARP: no handshake on any endpoint (UDP to Cloudflare blocked?)" >&2
|
||||
}
|
||||
|
||||
enable_egress() {
|
||||
local profile_source profile_path existing_clients
|
||||
load_env
|
||||
@@ -368,6 +401,7 @@ enable_egress() {
|
||||
sysctl -w net.ipv4.ip_forward=1 >/dev/null
|
||||
systemctl enable "wg-quick@${WARP_IF}" >/dev/null 2>&1 || true
|
||||
systemctl restart "wg-quick@${WARP_IF}"
|
||||
pick_warp_endpoint
|
||||
clear_legacy_subnet_rule
|
||||
reapply_client_rules "$existing_clients"
|
||||
save_state "egress" "$profile_source" "" "" "" "$existing_clients"
|
||||
@@ -436,7 +470,7 @@ client_disable() {
|
||||
status_json() {
|
||||
load_env
|
||||
load_state
|
||||
local subnet service configured endpoint pubkey egress enabled mode profile_source relay_on
|
||||
local subnet service configured endpoint pubkey egress enabled mode profile_source relay_on handshake=-1
|
||||
subnet="$(wg_subnet)"
|
||||
service="$(systemctl is-active "wg-quick@${WARP_IF}" 2>/dev/null || true)"
|
||||
mode="${STATE_MODE:-}"
|
||||
@@ -453,6 +487,7 @@ status_json() {
|
||||
endpoint="$(awk -F'= *' '/^Endpoint *=/{print $2; exit}' "$WARP_CONF" 2>/dev/null || true)"
|
||||
pubkey="$(wg show "${WARP_IF}" public-key 2>/dev/null || true)"
|
||||
egress="$(curl -4 --interface "${WARP_IF}" -fsS --max-time 6 https://api.ipify.org 2>/dev/null || true)"
|
||||
handshake="$(warp_handshake_age)"
|
||||
[ -n "$profile_source" ] || profile_source="$( [ -s "$CUSTOM_PROFILE" ] && echo custom || echo free )"
|
||||
fi
|
||||
|
||||
@@ -469,9 +504,9 @@ status_json() {
|
||||
enabled=true
|
||||
fi
|
||||
|
||||
python3 - "$enabled" "$configured" "$service" "$subnet" "$endpoint" "$pubkey" "$egress" "$mode" "$profile_source" "${STATE_RELAY_PORTS:-}" "${STATE_RELAY_TARGET:-}" "${STATE_RELAY_TARGET_IP:-}" "$( [ -s "$CUSTOM_PROFILE" ] && echo true || echo false )" "$relay_on" "$( [ -s "$LICENSE_FILE" ] && echo true || echo false )" "${STATE_CLIENTS:-}" <<'PY'
|
||||
python3 - "$enabled" "$configured" "$service" "$subnet" "$endpoint" "$pubkey" "$egress" "$mode" "$profile_source" "${STATE_RELAY_PORTS:-}" "${STATE_RELAY_TARGET:-}" "${STATE_RELAY_TARGET_IP:-}" "$( [ -s "$CUSTOM_PROFILE" ] && echo true || echo false )" "$relay_on" "$( [ -s "$LICENSE_FILE" ] && echo true || echo false )" "${STATE_CLIENTS:-}" "$handshake" <<'PY'
|
||||
import json, sys
|
||||
enabled, configured, service, subnet, endpoint, pubkey, egress, mode, profile_source, relay_ports, relay_target, relay_target_ip, custom_saved, relay_on, license_saved, clients_csv = sys.argv[1:]
|
||||
enabled, configured, service, subnet, endpoint, pubkey, egress, mode, profile_source, relay_ports, relay_target, relay_target_ip, custom_saved, relay_on, license_saved, clients_csv, handshake = sys.argv[1:]
|
||||
if relay_on == "true" and service != "active":
|
||||
service = "relay"
|
||||
print(json.dumps({
|
||||
@@ -484,6 +519,7 @@ print(json.dumps({
|
||||
"endpoint": endpoint,
|
||||
"public_key": pubkey,
|
||||
"egress_ipv4": egress,
|
||||
"handshake_age": int(handshake),
|
||||
"relay_enabled": relay_on == "true",
|
||||
"relay_ports": relay_ports,
|
||||
"relay_target": relay_target,
|
||||
|
||||
@@ -316,7 +316,7 @@ RAW="${PCA_RAW_BASE:-https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/${PCA_BRA
|
||||
PANEL_BOOTSTRAP="${MAIN_PROTO}://${MAIN_SERVER}:${MAIN_PORT}/server/api.py"
|
||||
curl -kfsSL "$PANEL_BOOTSTRAP" -o "$PHOBOS_DIR/server/api.py" \
|
||||
|| curl -fsSL "$RAW/server/api.py" -o "$PHOBOS_DIR/server/api.py" \
|
||||
|| curl -fsSL "https://raw.githubusercontent.com/andrey271192/PCA_Phobos/${PCA_BRANCH}/server/api.py" -o "$PHOBOS_DIR/server/api.py"
|
||||
|| curl -fsSL "https://raw.githubusercontent.com/adleibzon-rgb/PCA_Phobos/${PCA_BRANCH}/server/api.py" -o "$PHOBOS_DIR/server/api.py"
|
||||
[ -s "$PHOBOS_DIR/server/api.py" ] || { echo "ERROR: api.py fetch failed"; exit 1; }
|
||||
|
||||
cat > /etc/systemd/system/phobos-api.service << EOF
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# ============================================================
|
||||
# PCA Phobos — полное удаление сервера (primary / panel node)
|
||||
#
|
||||
# bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/uninstall.sh)
|
||||
# bash <(curl -fsSL https://raw.githubusercontent.com/adleibzon-rgb/PCA_Phobos/main/uninstall.sh)
|
||||
#
|
||||
# Останавливает и удаляет: панель, обфускатор-сервисы, WireGuard wg0,
|
||||
# nginx-сайт, watchdog-cron, бинарники, iptables-правила, каталоги Phobos.
|
||||
@@ -23,8 +23,9 @@ if [ "${PURGE:-0}" != "1" ]; then
|
||||
printf "Точно удалить ВСЁ? [yes/NO]: "
|
||||
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
|
||||
# no tr: in C locale it mangles Cyrillic bytes
|
||||
ans=${ans//$'\r'/}; ans=${ans//[[:space:]]/}
|
||||
case "$ans" in yes|Yes|YES|y|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)
|
||||
|
||||
@@ -19,7 +19,7 @@ set -e
|
||||
PHOBOS_DIR=/opt/Phobos
|
||||
PANEL_DIR=/opt/phobos-panel
|
||||
PCA_GIT="${PCA_GIT:-https://git.bobyrev.com/PCA/PCA_Phobos}"
|
||||
GH_RAW="${GH_RAW:-https://raw.githubusercontent.com/andrey271192/PCA_Phobos}"
|
||||
GH_RAW="${GH_RAW:-https://raw.githubusercontent.com/adleibzon-rgb/PCA_Phobos}"
|
||||
BACKUPS="$PHOBOS_DIR/updates"
|
||||
VERFILE="$PANEL_DIR/.version"
|
||||
CUR=$(cat "$VERFILE" 2>/dev/null || echo "unknown")
|
||||
@@ -53,8 +53,9 @@ fetch_repo_file() { # $1=ref $2=repo_path $3=output ; Gitea first, GitHub fallb
|
||||
curl -fsSL -m25 -H "Authorization: token $GH_TOKEN" "$PCA_GIT_DEV/raw/branch/$ref/$rp?cb=$(date +%s)" -o "$out" 2>/dev/null && return 0
|
||||
fi;;
|
||||
esac
|
||||
curl -fsSL -m25 "$PCA_GIT/raw/$kind/$ref/$rp?cb=$(date +%s)" -o "$out" 2>/dev/null && return 0
|
||||
curl -fsSL -m25 "$GH_RAW/$ref/$rp?cb=$(date +%s)" -o "$out"
|
||||
# GitHub first (Gitea sits on one home IP and is cut for some providers, 09.33), Gitea fallback
|
||||
curl -fsSL -m25 "$GH_RAW/$ref/$rp?cb=$(date +%s)" -o "$out" 2>/dev/null && return 0
|
||||
curl -fsSL -m25 "$PCA_GIT/raw/$kind/$ref/$rp?cb=$(date +%s)" -o "$out"
|
||||
}
|
||||
|
||||
fetch_repo_text() { # $1=ref $2=repo_path
|
||||
@@ -69,6 +70,7 @@ fetch_repo_text() { # $1=ref $2=repo_path
|
||||
managed_files() {
|
||||
cat <<LIST
|
||||
$PANEL_DIR/app.py|app.py
|
||||
$PANEL_DIR/APPS|APPS
|
||||
$PHOBOS_DIR/repo/server/scripts/lib-core.sh|overlay/lib-core.sh
|
||||
$PHOBOS_DIR/repo/server/scripts/phobos-client.sh|overlay/phobos-client.sh
|
||||
$PHOBOS_DIR/repo/client/templates/install-router.sh.template|overlay/install-router.sh.template
|
||||
|
||||
Reference in New Issue
Block a user