diff --git a/APPS b/APPS new file mode 100644 index 0000000..f2de949 --- /dev/null +++ b/APPS @@ -0,0 +1,3 @@ +Android (PCA Phobos VPN)=1.4.6 +iPhone (TestFlight)=1.4.5 +Windows=1.4.5-beta diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dc8d52..b94c0f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/README.md b/README.md index cfa826e..b9db340 100644 --- a/README.md +++ b/README.md @@ -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 / балансировки) diff --git a/VERSION b/VERSION index ac9f79c..079d7f6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.10 +1.4.11 diff --git a/app.py b/app.py index 3e4a402..82d78a3 100644 --- a/app.py +++ b/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*\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")}: {tr("проверяется…", "checking…")}' + def b(v): + col = "#e66" if v in GOOGLE_CLOSED else "var(--text)" + return f'{v or "—"}' + 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'({ago} {tr("мин назад", "min ago")})') + if others: + html += f'
{tr("другие IPv4", "other IPv4")}: {others}' + bad = [v for v in (g.get("v4"), g.get("v6")) if v in GOOGLE_CLOSED] + if bad: + html += (f'
{tr("Google считает адрес", "Google treats the address as")} «{", ".join(bad)}» — ' + f'{tr("Gemini и часть сервисов Google у клиентов не откроются", "Gemini and some Google services will not open for clients")}') + 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)} CONTENT