diff --git a/README.md b/README.md index bcfa9fa..c1a9fc3 100644 --- a/README.md +++ b/README.md @@ -148,3 +148,12 @@ PCA Phobos — это веб-панель и turnkey-инсталлятор по - [**wg-obfuscator**](https://github.com/ClusterM/wg-obfuscator) (ClusterM) — обфускация WireGuard-трафика. Поддержать автора: [Boosty](https://boosty.to/cluster) ❤️ Спасибо авторам за отличные инструменты. + +## Клиенты для телефона + +В таблице клиентов у каждого клиента есть кнопки: + +- 📱 **Android** — `phobos://`-ссылка + QR для приложения **PhobosWG** (с обфускацией). Скан QR → импорт. +- 🍎 **iPhone / iOS** — обычный WireGuard-конфиг + QR для официального WireGuard (БЕЗ обфускации). Требует открытого порта 51820 на сервере (поставь `ALLOW_PLAIN_WG=1` при установке, либо удали правило `iptables ... 51820 ... DROP`). + +> Для каждого устройства создавай **отдельного клиента** (свой ключ и IP). Один конфиг на двух устройствах = конфликт ключей. diff --git a/app.py b/app.py index c5f2b06..805d15e 100644 --- a/app.py +++ b/app.py @@ -403,6 +403,49 @@ def generate_failover_conf_for_client(client_id): return "\n".join(lines) + "\n" +def build_phone_config(cid, mode="android"): + """Assemble a phone client config from the client's files. + android = WireGuard + [instance] obfuscator -> phobos:// link (PhobosWG app). + ios = plain WireGuard (no obfuscation), Endpoint -> server:51820.""" + import base64, urllib.parse + cdir = os.path.join(CLIENTS_DIR, cid) + wgpath = os.path.join(cdir, cid + ".conf") + instpath = os.path.join(cdir, "wg-obfuscator.conf") + if not os.path.exists(wgpath): + return None, None + wg = open(wgpath).read().rstrip() + if mode == "ios": + server_ip = SERVER_IP + try: + for ln in open(instpath): + if ln.strip().startswith("target"): + server_ip = ln.split("=", 1)[1].strip().split(":")[0] + break + except Exception: + pass + out = [] + for ln in wg.split("\n"): + out.append("Endpoint = %s:51820" % server_ip if ln.strip().startswith("Endpoint") else ln) + return "\n".join(out) + "\n", None + inst = "" + if os.path.exists(instpath): + inst = open(instpath).read().strip() + conf = wg + "\n\n" + inst + "\n" + b64 = base64.urlsafe_b64encode(conf.encode()).decode().rstrip("=") + link = "phobos://" + b64 + "#" + urllib.parse.quote(cid) + return conf, link + + +def qr_datauri(data): + try: + import qrcode, io, base64 + buf = io.BytesIO() + qrcode.make(data).save(buf, format="PNG") + return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode() + except Exception: + return "" + + def fanout_router_config(client_id): """Push this client's failover.conf to every secondary server's agent so routers can PULL it through the tunnel (10.25.0.1:8444) — survives a public @@ -960,6 +1003,8 @@ def clients_page(): + 📱 + 🍎 {h_del} """ @@ -1210,6 +1255,36 @@ def sync_peer_to_all_servers(public_key, allowed_ips, action="add"): sync_peer_to_server(srv, public_key, allowed_ips, action) +@app.route("/client//phone/") +@auth_required +def client_phone(cid, mode): + if mode not in ("android", "ios"): + mode = "android" + conf, link = build_phone_config(cid, mode) + if conf is None: + return ("client not found", 404) + qr = qr_datauri(link if (mode == "android" and link) else conf) + if mode == "android": + title = tr("Android — PhobosWG (с обфускацией)", "Android — PhobosWG (obfuscated)") + extra = (f'

{tr("phobos:// ссылка — импорт в PhobosWG:", "phobos:// link — import into PhobosWG:")}

' + f'') + else: + title = tr("iPhone / iOS WireGuard (без обфускации)", "iPhone / iOS WireGuard (plain, no obfuscation)") + extra = (f'

{tr("⚠ Обычный WireGuard без маскировки (Endpoint → :51820). На сервере должен быть открыт порт 51820.", "⚠ Plain WireGuard, no obfuscation (Endpoint → :51820). Port 51820 must be open on the server.")}

') + html = f""" +
+ {nav('clients')} +
+

{cid} — {title}

+ QR
+

{tr("Отсканируй QR в приложении или импортируй конфиг:", "Scan the QR in the app or import the config:")}

+ {extra} + +

{tr("← назад", "← back")}

+
""" + return render(html) + + @app.route("/api/router-config/") def router_config(client_id): """NAT-friendly config pull: router fetches its own failover.conf. diff --git a/install.sh b/install.sh index 9b4f167..b436aca 100755 --- a/install.sh +++ b/install.sh @@ -54,7 +54,7 @@ echo "[1/9] dependencies..." export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq wireguard wireguard-tools iptables jq curl git \ - python3 python3-flask gunicorn nginx cron >/dev/null + python3 python3-flask gunicorn nginx cron python3-qrcode >/dev/null systemctl enable cron -q 2>/dev/null || true; systemctl start cron 2>/dev/null || true # ── 2. wg-obfuscator binary (Ground-Zerro) ── @@ -107,8 +107,13 @@ systemctl restart wg-quick@wg0 # ── 5. obfuscator services (multi-port) ── echo "[5/9] obfuscator services..." OBF_KEY=$(head -c 32 /dev/urandom | base64 | tr -d '/+=' | head -c 32) -iptables -C INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP 2>/dev/null \ - || iptables -A INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP +# Block direct WG (force obfuscation) unless ALLOW_PLAIN_WG=1 (e.g. for iOS WireGuard) +if [ -z "${ALLOW_PLAIN_WG:-}" ]; then + iptables -C INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP 2>/dev/null \ + || iptables -A INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP +else + iptables -D INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP 2>/dev/null || true +fi IFS=',' read -ra PORTS <<< "$OBF_PORTS" for PORT in "${PORTS[@]}"; do cat > "$PHOBOS_DIR/server/wg-obfuscator-${PORT}.conf" <