diff --git a/CHANGELOG.md b/CHANGELOG.md index a5cc791..86c62d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,13 @@ phobos-update beta # или: phobos-update dev --- +## 1.4.1 — резервный TCP-канал, «РФ напрямую» + +- **feat:** резервный канал TCP: если провайдер режет UDP, клиент сам переходит на TCP (порт 443, иначе 8443/2096/4433). Сервер: служба `phobos-tcp` (`udptcp`), ставится и включается автоматически при установке и обновлении. Проверено реальной блокировкой UDP: переключение за ~15 с, без блокировки остаётся UDP. +- **feat (Android 1.4.2):** «Российские сайты — напрямую, без VPN» (по умолчанию), выбор канала Авто/UDP/TCP, журнал TCP-канала. +- **fix:** установка и обновления с git.bobyrev.com (GitHub — запасной), каскад раздаёт панель. +- Ограничения: TCP-резерв пока только на основном сервере (не на вторых) и только в приложении PCA Phobos VPN (роутеры и PhobosWG — по-прежнему UDP с перебором портов). + ## 1.4.0 — единая версия (stable = beta = dev) Три канала сведены в одну версию: всё из dev (WARP, ttyd, бэкап, маршрут Keenetic, параметры обфускатора) diff --git a/README.md b/README.md index a07a16a..179705b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ - **Замер скорости** — сервера (реальная скачка/отдача, кнопка ⚡ на «Серверах»), клиента (живая скорость в карточке + страница `http://10.25.0.1:<порт>/speedtest` на устройстве через VPN). - **Домен панели** — `https://ваш-домен/` с бесплатным сертификатом Let's Encrypt; старый адрес `http://IP:порт` продолжает работать. - **Адрес для клиентов и каскад** — клиенты подключаются по домену или через входной сервер (например, в РФ) → ваш сервер; роутеры получают адрес сами. +- **Резервный TCP-канал** — если провайдер режет UDP, клиент сам переходит на TCP (порт 443), обфускация сохраняется. - **Сторож** — блокировка зондов и сканеров РКН (nftables, список обновляется раз в сутки). - **WARP / WARP+** — выпуск клиентов в интернет через Cloudflare WARP (всем или выборочно), relay-режим. - **Бэкап и восстановление** сервера из панели; **терминал роутера в браузере** (ttyd/RCI) за NAT. @@ -136,8 +137,8 @@ bash <(curl -fsSL https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/server/ ## Приложения для Android -- **PCA Phobos VPN** — клиент: включает VPN одной кнопкой, импорт `phobos://` из панели, встроенный обфускатор, **раздельный трафик** (весь / только выбранные приложения / все кроме выбранных), журнал, проверка скорости. [Скачать APK](https://git.bobyrev.com/PCA/PCA_Phobos/releases/download/v1.4.0/PCA-Phobos-VPN.apk) · исходники `android-client/`. -- **PCA Phobos** (админ) — список ваших панелей, открываются в приложении. [Скачать APK](https://git.bobyrev.com/PCA/PCA_Phobos/releases/download/v1.4.0/PCA-Phobos.apk) · исходники `android/`. +- **PCA Phobos VPN** — клиент: включает VPN одной кнопкой, импорт `phobos://` из панели, встроенный обфускатор, **раздельный трафик** (весь / только выбранные приложения / все кроме выбранных), **российские сайты напрямую** (по умолчанию), **канал Авто/UDP/TCP** (при блокировке UDP — сам на TCP), журнал, проверка скорости. [Скачать APK](https://git.bobyrev.com/PCA/PCA_Phobos/releases/download/v1.4.1/PCA-Phobos-VPN.apk) · исходники `android-client/`. +- **PCA Phobos** (админ) — список ваших панелей, открываются в приложении. [Скачать APK](https://git.bobyrev.com/PCA/PCA_Phobos/releases/download/v1.4.1/PCA-Phobos.apk) · исходники `android/`. ## Клиенты для телефона diff --git a/VERSION b/VERSION index 88c5fb8..347f583 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.0 +1.4.1 diff --git a/android-client/AndroidManifest.xml b/android-client/AndroidManifest.xml index ab84b31..495a5a3 100644 --- a/android-client/AndroidManifest.xml +++ b/android-client/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="142" + android:versionName="1.4.2"> diff --git a/android-client/build.sh b/android-client/build.sh index cc2c565..d4a6477 100755 --- a/android-client/build.sh +++ b/android-client/build.sh @@ -17,7 +17,8 @@ LIBS="build/aar/classes.jar:$(ls libs/*.jar | tr '\n' ':')" "$BT/aapt2" link -o build/base.apk -I "$JAR" --manifest AndroidManifest.xml -A assets \ --java build/gen build/res/*.flat --min-sdk-version 24 --target-sdk-version 35 javac -source 11 -target 11 -nowarn -classpath "$JAR:$LIBS" -d build/classes \ - $(find src build/gen -name '*.java') 2>&1 | grep -v "bootstrap classpath\|^Note:" || true + $(find src build/gen -name '*.java') 2>&1 | grep -v "bootstrap classpath\|^Note:\|^warning: \[options\]\|^1 warning" || true +[ -f build/classes/com/pca/phobos/vpn/MainActivity.class ] || { echo "javac failed"; exit 1; } "$BT/d8" --release --lib "$JAR" --min-api 24 --output build/dex \ $(find build/classes -name '*.class') build/aar/classes.jar libs/*.jar cp build/base.apk build/unsigned.apk @@ -25,7 +26,7 @@ cp build/base.apk build/unsigned.apk for abi in arm64-v8a armeabi-v7a; do mkdir -p "build/apk/lib/$abi" cp build/aar/jni/$abi/*.so "build/apk/lib/$abi/" - cp "jni/$abi/libwgobfuscator.so" "build/apk/lib/$abi/" + cp "jni/$abi/libwgobfuscator.so" "jni/$abi/libudptcp.so" "build/apk/lib/$abi/" done (cd build/apk && zip -qr ../unsigned.apk lib) "$BT/zipalign" -f 4 build/unsigned.apk build/aligned.apk diff --git a/android-client/jni/arm64-v8a/libudptcp.so b/android-client/jni/arm64-v8a/libudptcp.so new file mode 100755 index 0000000..b927a32 Binary files /dev/null and b/android-client/jni/arm64-v8a/libudptcp.so differ diff --git a/android-client/jni/armeabi-v7a/libudptcp.so b/android-client/jni/armeabi-v7a/libudptcp.so new file mode 100755 index 0000000..cc4a3de Binary files /dev/null and b/android-client/jni/armeabi-v7a/libudptcp.so differ diff --git a/android-client/src/com/pca/phobos/vpn/MainActivity.java b/android-client/src/com/pca/phobos/vpn/MainActivity.java index d5e1a01..ec76c39 100644 --- a/android-client/src/com/pca/phobos/vpn/MainActivity.java +++ b/android-client/src/com/pca/phobos/vpn/MainActivity.java @@ -59,7 +59,9 @@ public class MainActivity extends Activity { static final String MODE_ALL = "all", MODE_ONLY = "only", MODE_EXCEPT = "except"; static GoBackend backend; - static Process obfuscator; + static Process obfuscator, relay; + static final int TCP_LPORT = 13256; + static volatile String transport = ""; // "UDP" | "TCP" while up static final Tunnel TUNNEL = new Tunnel() { public String getName() { return "phobos"; } public void onStateChange(Tunnel.State s) { } @@ -215,17 +217,27 @@ public class MainActivity extends Activity { private static String getPackageNameSafe() { return "com.pca.phobos.vpn"; } - private String obfPart(String conf) { + private String obfPart(String conf, boolean viaTcp) { StringBuilder sb = new StringBuilder("[instance]\nsource-if = 127.0.0.1\nsource-lport = " + LPORT + "\n"); for (String ln : section(conf, "[instance]")) { String k = key(ln); if (k.equals("source-if") || k.equals("source-lport") || k.equals("verbose")) continue; + if (viaTcp && k.equals("target")) ln = "target = 127.0.0.1:" + TCP_LPORT; sb.append(ln).append('\n'); } sb.append("verbose = INFO\n"); return sb.toString(); } + /** "# pca-tcp = 443" written by the panel when the server has the TCP fallback. */ + private static String tcpPort(String conf) { + for (String ln : conf.split("\n")) { + String t = ln.trim(); + if (t.startsWith("#") && t.contains("pca-tcp") && t.contains("=")) return val(t); + } + return ""; + } + private String target(String conf) { for (String ln : section(conf, "[instance]")) if (key(ln).equals("target")) return val(ln); return "?"; @@ -250,24 +262,39 @@ public class MainActivity extends Activity { else if (req == 7) toast("Без разрешения на VPN подключиться нельзя"); } + /** Start (or restart) obfuscator; via TCP it dials the local udptcp relay. */ + private void startTransport(String conf, boolean viaTcp) throws Exception { + stopObfuscator(); + String lib = getApplicationInfo().nativeLibraryDir; + if (viaTcp) { + String host = target(conf).replaceAll(":\\d+$", ""); + relay = new ProcessBuilder(lib + "/libudptcp.so", "-c", "127.0.0.1:" + TCP_LPORT, host + ":" + tcpPort(conf)) + .redirectErrorStream(true).redirectOutput(new File(getFilesDir(), "tcp.log")).start(); + } + File cfg = new File(getFilesDir(), "obf.conf"); + try (FileOutputStream o = new FileOutputStream(cfg)) { o.write(obfPart(conf, viaTcp).getBytes(StandardCharsets.UTF_8)); } + obfuscator = new ProcessBuilder(lib + "/libwgobfuscator.so", "-c", cfg.getAbsolutePath()) + .redirectErrorStream(true).redirectOutput(new File(getFilesDir(), "obf.log")).start(); + Thread.sleep(600); + if (!obfuscator.isAlive()) throw new IllegalStateException("обфускатор завершился, код " + obfuscator.exitValue() + " — см. «Журнал»"); + if (relay != null && !relay.isAlive()) throw new IllegalStateException("TCP-канал не запустился — см. «Журнал»"); + transport = viaTcp ? "TCP" : "UDP"; + log("transport " + transport + " -> " + target(conf) + (viaTcp ? " tcp:" + tcpPort(conf) : "")); + } + private void up() { try { String conf = prefs.getString("conf", ""); - File cfg = new File(getFilesDir(), "obf.conf"); - try (FileOutputStream o = new FileOutputStream(cfg)) { o.write(obfPart(conf).getBytes(StandardCharsets.UTF_8)); } - stopObfuscator(); - String bin = getApplicationInfo().nativeLibraryDir + "/libwgobfuscator.so"; - obfuscator = new ProcessBuilder(bin, "-c", cfg.getAbsolutePath()) - .redirectErrorStream(true) - .redirectOutput(new File(getFilesDir(), "obf.log")) - .start(); - Thread.sleep(600); - if (!obfuscator.isAlive()) throw new IllegalStateException("обфускатор завершился, код " + obfuscator.exitValue() + " — см. «Журнал»"); + String mode = prefs.getString("transport", "auto"); + boolean tcpAvail = !tcpPort(conf).isEmpty(); + if ("tcp".equals(mode) && !tcpAvail) throw new IllegalStateException("на сервере нет TCP-канала — обновите панель и заново импортируйте ссылку"); + startTransport(conf, "tcp".equals(mode)); Config c = Config.parse(new BufferedReader(new StringReader( wgPart(conf, prefs.getString("mode", MODE_ALL), prefs.getString("apps", ""))))); backend.setState(TUNNEL, Tunnel.State.UP, c); - log("up ok, target " + target(conf) + ", mode " + prefs.getString("mode", MODE_ALL)); - post("Подключено"); + log("up ok, mode " + prefs.getString("mode", MODE_ALL) + ", transport " + mode); + post("Подключено (" + transport + ")"); + if ("auto".equals(mode) && tcpAvail) watchUdp(conf); } catch (Throwable e) { stopObfuscator(); log("up failed: " + e); @@ -275,14 +302,30 @@ public class MainActivity extends Activity { } } + /** Auto: no bytes back over UDP within 15 s -> UDP is cut, move the obfuscator to TCP. */ + private void watchUdp(String conf) { + new Thread(() -> { + try { + Thread.sleep(15000); + if (!isUp() || !"UDP".equals(transport)) return; + if (backend.getStatistics(TUNNEL).totalRx() > 0) return; + log("UDP: no answer in 15 s -> switching to TCP"); + startTransport(conf, true); + post("UDP не отвечает — переключился на TCP"); + } catch (Throwable e) { log("watchUdp: " + e); } + }).start(); + } + private void down() { try { backend.setState(TUNNEL, Tunnel.State.DOWN, null); } catch (Throwable ignored) { } stopObfuscator(); + transport = ""; post("Отключено"); } private static void stopObfuscator() { if (obfuscator != null) { obfuscator.destroy(); obfuscator = null; } + if (relay != null) { relay.destroy(); relay = null; } } private void post(String msg) { ui.post(() -> { toast(msg); refresh(); }); } @@ -364,6 +407,20 @@ public class MainActivity extends Activity { ru.setChecked(prefs.getBoolean("ru_direct", true)); ru.setOnCheckedChangeListener((b, on) -> { prefs.edit().putBoolean("ru_direct", on).apply(); applyIfUp(); }); split.addView(ru, mw(4)); + split.addView(tv("Канал до сервера", text(), 14, true), mw(8)); + RadioGroup tg = new RadioGroup(this); + tg.setOrientation(RadioGroup.HORIZONTAL); + String tmode = prefs.getString("transport", "auto"); + for (String[] m : new String[][]{{"auto", "Авто"}, {"udp", "UDP"}, {"tcp", "TCP"}}) { + RadioButton r = new RadioButton(this); + r.setText(m[1]); + r.setTextColor(text()); + r.setId(View.generateViewId()); + r.setChecked(m[0].equals(tmode)); + r.setOnClickListener(v -> { prefs.edit().putString("transport", m[0]).apply(); applyIfUp(); }); + tg.addView(r); + } + split.addView(tg); int n = prefs.getString("apps", "").isEmpty() ? 0 : prefs.getString("apps", "").split(",").length; split.addView(button("Выбрать приложения (" + n + ")", 0xFF475569, v -> pickApps()), mw(8)); split.addView(tv("Изменения применяются сразу. Само приложение всегда идёт напрямую — так обфускатор держит связь с сервером.", @@ -385,7 +442,7 @@ public class MainActivity extends Activity { boolean up = isUp(); power.setText(up ? "Отключить" : "Подключить"); power.setBackground(round(up ? 0xFF16A34A : 0xFF4F46E5, 90)); - status.setText(up ? "🟢 VPN включён" : "⚪ VPN выключен"); + status.setText(up ? "🟢 VPN включён" + (transport.isEmpty() ? "" : " · " + transport) : "⚪ VPN выключен"); if (up) { try { Statistics st = backend.getStatistics(TUNNEL); @@ -466,6 +523,7 @@ public class MainActivity extends Activity { private void showLog() { String t = "— приложение —\n" + tail(new File(getFilesDir(), "app.log"), 3000) + "\n— обфускатор —\n" + tail(new File(getFilesDir(), "obf.log"), 3000) + + "\n— TCP-канал —\n" + tail(new File(getFilesDir(), "tcp.log"), 1500) + "\nобфускатор запущен: " + (obfuscator != null && obfuscator.isAlive()); TextView v = tv(t, text(), 12, false); v.setTextIsSelectable(true); diff --git a/app.py b/app.py index 6c7185c..d04f4d0 100644 --- a/app.py +++ b/app.py @@ -37,7 +37,7 @@ 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" -APK_TAG = "v1.4.0" +APK_TAG = "v1.4.1" WARP_DIR = f"{PHOBOS_DIR}/server/warp" WARP_CUSTOM_PROFILE_FILE = f"{WARP_DIR}/custom-profile.conf" WARP_LICENSE_FILE = f"{WARP_DIR}/license.key" @@ -1390,6 +1390,10 @@ def build_phone_config(cid, mode="android"): ("target = " + _swap_host(ln.split("=", 1)[1].strip(), client_endpoint)) if ln.strip().startswith("target") and "=" in ln else ln for ln in inst.split("\n")) + tcp_port = _load_server_env().get("TCP_PORT", "") + if tcp_port and "pca-tcp" not in inst: + # comment: PhobosWG/wg-obfuscator ignore it, PCA Phobos VPN uses it for the TCP fallback + inst += "\n# pca-tcp = " + tcp_port conf = wg + "\n\n" + inst + "\n" payload = _prepare_phoboswg_payload(conf) b64 = base64.urlsafe_b64encode(payload.encode()).decode() diff --git a/install.sh b/install.sh index 8d03737..bc0c16a 100755 --- a/install.sh +++ b/install.sh @@ -243,6 +243,8 @@ fetch server/phobos-cascade.sh "$PHOBOS_DIR/server/phobos-cascade.s fetch server/phobos-domain.sh "$PHOBOS_DIR/server/phobos-domain.sh" && chmod +x "$PHOBOS_DIR/server/phobos-domain.sh" && ln -sf "$PHOBOS_DIR/server/phobos-domain.sh" /usr/local/bin/phobos-domain fetch server/phobos-limit.sh "$PHOBOS_DIR/server/phobos-limit.sh" && chmod +x "$PHOBOS_DIR/server/phobos-limit.sh" fetch server/phobos-speedtest.sh "$PHOBOS_DIR/server/phobos-speedtest.sh" && chmod +x "$PHOBOS_DIR/server/phobos-speedtest.sh" +fetch server/phobos-tcp.sh "$PHOBOS_DIR/server/phobos-tcp.sh" && chmod +x "$PHOBOS_DIR/server/phobos-tcp.sh" && ln -sf "$PHOBOS_DIR/server/phobos-tcp.sh" /usr/local/bin/phobos-tcp +fetch "udptcp/bin/udptcp-$OBF_ARCH" "$PHOBOS_DIR/bin/udptcp" && chmod +x "$PHOBOS_DIR/bin/udptcp" fetch server/phobos-health.sh "$PHOBOS_DIR/server/phobos-health.sh" && chmod +x "$PHOBOS_DIR/server/phobos-health.sh" fetch server/phobos-self-check.sh "$PHOBOS_DIR/server/phobos-self-check.sh" && chmod +x "$PHOBOS_DIR/server/phobos-self-check.sh" && ln -sf "$PHOBOS_DIR/server/phobos-self-check.sh" /usr/local/bin/phobos-self-check fetch server/phobos-pull.sh "$PHOBOS_DIR/server/phobos-pull.sh" && chmod +x "$PHOBOS_DIR/server/phobos-pull.sh" @@ -298,6 +300,9 @@ WantedBy=multi-user.target EOF systemctl daemon-reload; systemctl enable phobos-panel -q; systemctl restart phobos-panel +# TCP fallback for clients whose UDP is cut (port 443/8443/... picked automatically) +"$PHOBOS_DIR/server/phobos-tcp.sh" enable || echo " WARN: TCP fallback not enabled" + # ── 8. nginx (serve /init + /packages over plain HTTP for routers) ── echo "[8/9] nginx..." rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true diff --git a/server/phobos-tcp.sh b/server/phobos-tcp.sh new file mode 100755 index 0000000..25fbcc5 --- /dev/null +++ b/server/phobos-tcp.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# PCA Phobos — TCP fallback: clients whose UDP is cut reach the obfuscator over TCP. +# udptcp -s 0.0.0.0: -> 127.0.0.1: (UDP). +# phobos-tcp.sh enable [port] pick a free port (443, 8443, 2096) unless given; idempotent +# phobos-tcp.sh disable | status +set -uo pipefail +PHOBOS_DIR=/opt/Phobos +ENV="$PHOBOS_DIR/server/server.env" +BIN="$PHOBOS_DIR/bin/udptcp" + +env_get() { grep -E "^$1=" "$ENV" 2>/dev/null | tail -1 | cut -d= -f2-; } +env_set() { + grep -v -E "^$1=" "$ENV" > "$ENV.tmp" 2>/dev/null || true + echo "$1=$2" >> "$ENV.tmp" && mv "$ENV.tmp" "$ENV" +} +port_busy() { ss -lntH "sport = :$1" 2>/dev/null | grep -q .; } + +enable() { + [ -x "$BIN" ] || { echo "udptcp binary missing: $BIN (phobos-update)" >&2; exit 1; } + local port="${1:-$(env_get TCP_PORT)}" obf + obf=$(env_get OBFUSCATOR_PORTS | cut -d, -f1); obf="${obf:-2083}" + # keep our own port if the service already holds it + if [ -z "$port" ] || { port_busy "$port" && ! systemctl is-active -q phobos-tcp; }; then + port="" + for p in 443 8443 2096 4433; do port_busy "$p" || { port=$p; break; }; done + fi + [ -n "$port" ] || { echo "no free TCP port (443/8443/2096/4433)" >&2; exit 1; } + env_set TCP_PORT "$port" + cat > /etc/systemd/system/phobos-tcp.service < obfuscator :$obf) +After=network-online.target +Wants=network-online.target +[Service] +ExecStart=$BIN -s 0.0.0.0:$port 127.0.0.1:$obf +Restart=always +RestartSec=3 +[Install] +WantedBy=multi-user.target +EOF + systemctl daemon-reload + systemctl enable -q phobos-tcp + systemctl restart phobos-tcp + sleep 1 + status +} + +disable() { + systemctl disable --now phobos-tcp >/dev/null 2>&1 || true + rm -f /etc/systemd/system/phobos-tcp.service + systemctl daemon-reload + echo "tcp fallback: disabled" +} + +status() { + printf '{"enabled": %s, "port": "%s"}\n' \ + "$(systemctl is-active -q phobos-tcp && echo true || echo false)" "$(env_get TCP_PORT)" +} + +case "${1:-status}" in + enable) enable "${2:-}" ;; + disable) disable ;; + status) status ;; + *) echo "usage: $0 enable [port]|disable|status"; exit 2 ;; +esac diff --git a/udptcp/README.md b/udptcp/README.md new file mode 100644 index 0000000..7e33b41 --- /dev/null +++ b/udptcp/README.md @@ -0,0 +1,6 @@ +# udptcp — резервный TCP-канал PCA Phobos + +UDP-пакеты (уже обфусцированные) внутри TCP: кадр = 2 байта длины + пакет. +- сервер: `udptcp -s 0.0.0.0:443 127.0.0.1:2083` (служба `phobos-tcp`, порт в server.env `TCP_PORT`); +- клиент: `udptcp -c 127.0.0.1:13256 <сервер>:443`, обфускатор клиента шлёт на 127.0.0.1:13256. +Сборка: `gcc -O2 -static -pthread -o bin/udptcp-x86_64 udptcp.c` (+ aarch64-linux-gnu-gcc), Android — NDK (см. android-client/README.md). diff --git a/udptcp/bin/udptcp-aarch64 b/udptcp/bin/udptcp-aarch64 new file mode 100755 index 0000000..7dfdaec Binary files /dev/null and b/udptcp/bin/udptcp-aarch64 differ diff --git a/udptcp/bin/udptcp-x86_64 b/udptcp/bin/udptcp-x86_64 new file mode 100755 index 0000000..e59ab79 Binary files /dev/null and b/udptcp/bin/udptcp-x86_64 differ diff --git a/udptcp/udptcp.c b/udptcp/udptcp.c new file mode 100644 index 0000000..bda5e89 --- /dev/null +++ b/udptcp/udptcp.c @@ -0,0 +1,175 @@ +/* + * udptcp — PCA Phobos fallback transport: UDP datagrams carried over TCP. + * Frames on the TCP stream: 2-byte big-endian length + payload. + * + * server: udptcp -s + * each TCP connection gets its own UDP socket to the target + * (the local wg-obfuscator), so the obfuscator sees one client per conn. + * client: udptcp -c + * local UDP peer (client wg-obfuscator) <-> one TCP connection, reconnects. + * + * Plain C, no deps: builds for Linux servers, Android (NDK) and iOS. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAXPKT 65535 + +static int parse_addr(const char *s, struct sockaddr_storage *ss, socklen_t *len, int socktype) { + char host[256]; + const char *colon = strrchr(s, ':'); + if (!colon || colon == s || (size_t)(colon - s) >= sizeof host) return -1; + memcpy(host, s, colon - s); + host[colon - s] = 0; + struct addrinfo hints = {0}, *res; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = socktype; + if (getaddrinfo(host, colon + 1, &hints, &res) != 0) return -1; + memcpy(ss, res->ai_addr, res->ai_addrlen); + *len = res->ai_addrlen; + freeaddrinfo(res); + return 0; +} + +static int read_full(int fd, unsigned char *buf, size_t n) { + size_t got = 0; + while (got < n) { + ssize_t r = read(fd, buf + got, n - got); + if (r <= 0) return -1; + got += (size_t)r; + } + return 0; +} + +static int write_frame(int fd, const unsigned char *p, size_t n) { + unsigned char hdr[2] = {(unsigned char)(n >> 8), (unsigned char)(n & 0xff)}; + if (write(fd, hdr, 2) != 2) return -1; + size_t sent = 0; + while (sent < n) { + ssize_t w = write(fd, p + sent, n - sent); + if (w <= 0) return -1; + sent += (size_t)w; + } + return 0; +} + +static void tune_tcp(int fd) { + int one = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one); + setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof one); +} + +/* ── server ─────────────────────────────────────────────────────────────── */ +static struct sockaddr_storage g_target; +static socklen_t g_target_len; + +static void *serve_conn(void *arg) { + int tcp = (int)(long)arg; + unsigned char buf[MAXPKT + 2]; + int udp = socket(g_target.ss_family, SOCK_DGRAM, 0); + if (udp < 0 || connect(udp, (struct sockaddr *)&g_target, g_target_len) < 0) goto out; + tune_tcp(tcp); + struct pollfd p[2] = {{tcp, POLLIN, 0}, {udp, POLLIN, 0}}; + for (;;) { + if (poll(p, 2, 300000) <= 0) break; /* 5 min idle -> drop */ + if (p[0].revents & (POLLIN | POLLHUP | POLLERR)) { + unsigned char h[2]; + if (read_full(tcp, h, 2) < 0) break; + size_t n = ((size_t)h[0] << 8) | h[1]; + if (n == 0 || read_full(tcp, buf, n) < 0) break; + send(udp, buf, n, 0); + } + if (p[1].revents & POLLIN) { + ssize_t n = recv(udp, buf, MAXPKT, 0); + if (n > 0 && write_frame(tcp, buf, (size_t)n) < 0) break; + } + } +out: + if (udp >= 0) close(udp); + close(tcp); + return NULL; +} + +static int run_server(const char *listen_s, const char *target_s) { + struct sockaddr_storage la; socklen_t ll; + if (parse_addr(listen_s, &la, &ll, SOCK_STREAM) || parse_addr(target_s, &g_target, &g_target_len, SOCK_DGRAM)) { + fprintf(stderr, "bad address\n"); return 2; + } + int ls = socket(la.ss_family, SOCK_STREAM, 0), one = 1; + setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); + if (bind(ls, (struct sockaddr *)&la, ll) < 0 || listen(ls, 128) < 0) { perror("listen"); return 1; } + fprintf(stderr, "udptcp server %s -> udp %s\n", listen_s, target_s); + for (;;) { + int c = accept(ls, NULL, NULL); + if (c < 0) { if (errno == EINTR) continue; perror("accept"); sleep(1); continue; } + pthread_t t; + if (pthread_create(&t, NULL, serve_conn, (void *)(long)c) == 0) pthread_detach(t); + else close(c); + } +} + +/* ── client ─────────────────────────────────────────────────────────────── */ +static int connect_tcp(const char *server_s) { + struct sockaddr_storage sa; socklen_t sl; + if (parse_addr(server_s, &sa, &sl, SOCK_STREAM)) return -1; /* re-resolve each time */ + int s = socket(sa.ss_family, SOCK_STREAM, 0); + if (s < 0) return -1; + if (connect(s, (struct sockaddr *)&sa, sl) < 0) { close(s); return -1; } + tune_tcp(s); + return s; +} + +static int run_client(const char *local_s, const char *server_s) { + struct sockaddr_storage la, peer; socklen_t ll, pl = 0; + if (parse_addr(local_s, &la, &ll, SOCK_DGRAM)) { fprintf(stderr, "bad address\n"); return 2; } + int us = socket(la.ss_family, SOCK_DGRAM, 0); + if (bind(us, (struct sockaddr *)&la, ll) < 0) { perror("bind"); return 1; } + fprintf(stderr, "udptcp client udp %s -> tcp %s\n", local_s, server_s); + unsigned char buf[MAXPKT + 2]; + int tcp = -1; + time_t last_try = 0; + for (;;) { + struct pollfd p[2] = {{us, POLLIN, 0}, {tcp, POLLIN, 0}}; + poll(p, tcp >= 0 ? 2 : 1, 1000); + if (p[0].revents & POLLIN) { + pl = sizeof peer; + ssize_t n = recvfrom(us, buf, MAXPKT, 0, (struct sockaddr *)&peer, &pl); + if (n > 0) { + if (tcp < 0 && time(NULL) != last_try) { + last_try = time(NULL); + tcp = connect_tcp(server_s); + fprintf(stderr, tcp >= 0 ? "connected\n" : "connect failed\n"); + } + if (tcp >= 0 && write_frame(tcp, buf, (size_t)n) < 0) { close(tcp); tcp = -1; } + } + } + if (tcp >= 0 && (p[1].revents & (POLLIN | POLLHUP | POLLERR))) { + unsigned char h[2]; + if (read_full(tcp, h, 2) < 0) { close(tcp); tcp = -1; continue; } + size_t n = ((size_t)h[0] << 8) | h[1]; + if (n == 0 || read_full(tcp, buf, n) < 0) { close(tcp); tcp = -1; continue; } + if (pl) sendto(us, buf, n, 0, (struct sockaddr *)&peer, pl); + } + } +} + +int main(int argc, char **argv) { + signal(SIGPIPE, SIG_IGN); + if (argc == 4 && !strcmp(argv[1], "-s")) return run_server(argv[2], argv[3]); + if (argc == 4 && !strcmp(argv[1], "-c")) return run_client(argv[2], argv[3]); + fprintf(stderr, "usage: udptcp -s \n" + " udptcp -c \n"); + return 2; +} diff --git a/update.sh b/update.sh index d626c88..a940d50 100755 --- a/update.sh +++ b/update.sh @@ -86,10 +86,25 @@ $PHOBOS_DIR/server/phobos-cascade.sh|server/phobos-cascade.sh $PHOBOS_DIR/server/phobos-domain.sh|server/phobos-domain.sh $PHOBOS_DIR/server/phobos-limit.sh|server/phobos-limit.sh $PHOBOS_DIR/server/phobos-speedtest.sh|server/phobos-speedtest.sh +$PHOBOS_DIR/server/phobos-tcp.sh|server/phobos-tcp.sh $PHOBOS_DIR/server/update.sh|update.sh LIST } +ensure_tcp_fallback() { # $1=ref ; primary only (secondaries have no panel) + [ -f "$PANEL_DIR/app.py" ] || return 0 + local arch tmp + arch=$(uname -m); case "$arch" in amd64) arch=x86_64;; arm64) arch=aarch64;; esac + tmp=$(mktemp) + if fetch_repo_file "$1" "udptcp/bin/udptcp-$arch" "$tmp" && [ -s "$tmp" ]; then + install -m755 "$tmp" "$PHOBOS_DIR/bin/udptcp" + fi + rm -f "$tmp" + [ -x "$PHOBOS_DIR/server/phobos-tcp.sh" ] && ln -sf "$PHOBOS_DIR/server/phobos-tcp.sh" /usr/local/bin/phobos-tcp + [ -x "$PHOBOS_DIR/bin/udptcp" ] && "$PHOBOS_DIR/server/phobos-tcp.sh" enable >/dev/null 2>&1 \ + && echo "TCP fallback: port $(grep '^TCP_PORT=' "$PHOBOS_DIR/server/server.env" | cut -d= -f2)" || true +} + run_self_check() { if [ -x "$PHOBOS_DIR/server/phobos-self-check.sh" ]; then ln -sf "$PHOBOS_DIR/server/phobos-self-check.sh" /usr/local/bin/phobos-self-check 2>/dev/null || true @@ -251,6 +266,7 @@ case "${1:-}" in apply_ref "$ref" ensure_runtime_deps run_self_check + ensure_tcp_fallback "$ref" refresh_router_packages echo "$target" > "$VERFILE" restart_panel_service