release: 1.4.1 — TCP fallback, Russian sites direct in the Android client
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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, параметры обфускатора)
|
||||
|
||||
@@ -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/`.
|
||||
|
||||
## Клиенты для телефона
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.pca.phobos.vpn"
|
||||
android:versionCode="141"
|
||||
android:versionName="1.4.1">
|
||||
android:versionCode="142"
|
||||
android:versionName="1.4.2">
|
||||
|
||||
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="35" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
android-client/jni/arm64-v8a/libudptcp.so
Executable file
BIN
android-client/jni/arm64-v8a/libudptcp.so
Executable file
Binary file not shown.
BIN
android-client/jni/armeabi-v7a/libudptcp.so
Executable file
BIN
android-client/jni/armeabi-v7a/libudptcp.so
Executable file
Binary file not shown.
@@ -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);
|
||||
|
||||
6
app.py
6
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()
|
||||
|
||||
@@ -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
|
||||
|
||||
65
server/phobos-tcp.sh
Executable file
65
server/phobos-tcp.sh
Executable file
@@ -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:<TCP_PORT> -> 127.0.0.1:<first obfuscator port> (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 <<EOF
|
||||
[Unit]
|
||||
Description=PCA Phobos TCP fallback (udptcp -> 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
|
||||
6
udptcp/README.md
Normal file
6
udptcp/README.md
Normal file
@@ -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).
|
||||
BIN
udptcp/bin/udptcp-aarch64
Executable file
BIN
udptcp/bin/udptcp-aarch64
Executable file
Binary file not shown.
BIN
udptcp/bin/udptcp-x86_64
Executable file
BIN
udptcp/bin/udptcp-x86_64
Executable file
Binary file not shown.
175
udptcp/udptcp.c
Normal file
175
udptcp/udptcp.c
Normal file
@@ -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 <listen_ip:port> <udp_target_ip:port>
|
||||
* 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_ip:port> <tcp_server_host:port>
|
||||
* local UDP peer (client wg-obfuscator) <-> one TCP connection, reconnects.
|
||||
*
|
||||
* Plain C, no deps: builds for Linux servers, Android (NDK) and iOS.
|
||||
*/
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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 <listen ip:port> <udp target ip:port>\n"
|
||||
" udptcp -c <local udp ip:port> <tcp server host:port>\n");
|
||||
return 2;
|
||||
}
|
||||
16
update.sh
16
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
|
||||
|
||||
Reference in New Issue
Block a user