From becd39da8446af126545993414423cdc1553d168 Mon Sep 17 00:00:00 2001 From: instant992 Date: Wed, 10 Jun 2026 23:56:31 +0400 Subject: [PATCH] Network-aware proxy selection and accurate VLESS ping - Show built-in servers whose name contains 'wifi' only on Wi-Fi and 'lte/mobile/4g/5g' only on mobile data; untagged servers show everywhere - Auto-enable, restore and live network-change handlers now pick a server matching the current network and switch automatically when it changes - Replace the MTProto proxy check (which always fails for VLESS+Reality ports and falsely reports 'unavailable') with a real TCP-connect latency probe to the server endpoint --- .../telegram/messenger/ApplicationLoader.java | 13 ++- .../telegram/messenger/XrayController.java | 103 +++++++++++++++++- .../org/telegram/ui/ProxyListActivity.java | 34 ++++++ 3 files changed, 147 insertions(+), 3 deletions(-) diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java index f87ebad1..5ed85b6c 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java @@ -432,8 +432,19 @@ public class ApplicationLoader extends Application { proxy = XrayController.createBuiltinProxy(); } } + // If the saved built-in proxy is tagged for the other network (e.g. a + // "WiFi" server while we're on mobile data now), switch to a matching one. + if (proxy != null && proxy.builtin && !XrayController.matchesCurrentNetwork(proxy)) { + SharedConfig.ProxyInfo match = XrayController.createBuiltinProxyForCurrentNetwork(); + if (match != null && XrayController.matchesCurrentNetwork(match)) { + proxy = match; + int localPort = match.vlessLocalPort > 0 ? match.vlessLocalPort : 10808; + prefs.edit().putInt("proxy_port", localPort).apply(); + } + } final SharedConfig.ProxyInfo proxyToStart = proxy; if (proxyToStart == null) return; // no builtin servers configured + SharedConfig.currentProxy = proxyToStart; new Thread(() -> { boolean ok = XrayController.start(proxyToStart.toVlessConfig()); @@ -448,7 +459,7 @@ public class ApplicationLoader extends Application { private static void autoEnableBuiltinProxy(android.content.SharedPreferences prefs) { SharedConfig.loadProxyList(); - SharedConfig.ProxyInfo proxy = XrayController.createBuiltinProxy(); + SharedConfig.ProxyInfo proxy = XrayController.createBuiltinProxyForCurrentNetwork(); if (proxy == null) { // No bundled servers configured in this build — mark as handled so we // don't keep probing on every launch. diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/XrayController.java b/TMessagesProj/src/main/java/org/telegram/messenger/XrayController.java index b1006dc8..66586ddb 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/XrayController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/XrayController.java @@ -13,6 +13,8 @@ import android.util.Log; import org.json.JSONObject; +import org.telegram.tgnet.ConnectionsManager; + import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; @@ -59,6 +61,63 @@ public class XrayController { return null; } + /** + * Whether a proxy is allowed on the current network based on its name: + * - names containing "wifi" are shown only on Wi-Fi; + * - names containing "lte" (or "mobile"/"4g"/"5g") only on mobile data; + * - names with neither keyword are always allowed. + * Non-builtin proxies are always allowed. + */ + public static boolean matchesCurrentNetwork(SharedConfig.ProxyInfo proxy) { + if (proxy == null || !proxy.builtin) return true; + String name = proxy.builtinName != null ? proxy.builtinName.toLowerCase(java.util.Locale.ROOT) : ""; + boolean taggedWifi = name.contains("wifi") || name.contains("wi-fi"); + boolean taggedMobile = name.contains("lte") || name.contains("mobile") + || name.contains("4g") || name.contains("5g") || name.contains("3g"); + if (!taggedWifi && !taggedMobile) { + return true; // untagged proxy works everywhere + } + boolean onWifi; + try { + onWifi = ApplicationLoader.isConnectedToWiFi(); + } catch (Throwable e) { + return true; // can't tell — don't hide anything + } + return onWifi ? taggedWifi : taggedMobile; + } + + /** First built-in proxy that matches the current network, or null. */ + public static SharedConfig.ProxyInfo createBuiltinProxyForCurrentNetwork() { + SharedConfig.ProxyInfo fallback = null; + for (XrayServers.Server s : XrayServers.SERVERS) { + SharedConfig.ProxyInfo p = fromServer(s); + if (p == null) continue; + if (fallback == null) fallback = p; + if (matchesCurrentNetwork(p)) return p; + } + return fallback; + } + + /** + * Measure a real TCP-connect latency to the proxy's server endpoint, in ms. + * Returns -1 if the host can't be reached within the timeout. + * + * For built-in VLESS+Reality servers this is far more accurate than + * Telegram's MTProto proxy check, which always fails against a VLESS port + * (it doesn't speak the MTProto proxy protocol) and so reports the server + * as "unavailable" even though it works fine once connected. + */ + public static long measureTcpLatency(String host, int port, int timeoutMs) { + if (host == null || host.isEmpty() || port <= 0) return -1; + long start = android.os.SystemClock.elapsedRealtime(); + try (Socket s = new Socket()) { + s.connect(new InetSocketAddress(host, port), timeoutMs); + return Math.max(1, android.os.SystemClock.elapsedRealtime() - start); + } catch (Throwable e) { + return -1; + } + } + private static SharedConfig.ProxyInfo fromServer(XrayServers.Server s) { if (s == null || s.address == null || s.address.isEmpty()) return null; SharedConfig.ProxyInfo p = SharedConfig.ProxyInfo.createVless( @@ -184,16 +243,38 @@ public class XrayController { // Only restart Xray if we actually had a different network before // (skip the initial onAvailable that fires right after registration) if (sXrayStartedOnce) { + // If the active proxy is a built-in one tagged for the + // other network (e.g. a "WiFi" server while we just moved + // to mobile data), switch to a server that matches the new + // network. Otherwise just restart the current config. + SharedConfig.ProxyInfo current = SharedConfig.currentProxy; VlessConfig cfg = sCurrentConfig; + SharedConfig.ProxyInfo switchTo = null; + if (current != null && current.builtin && !matchesCurrentNetwork(current)) { + SharedConfig.ProxyInfo match = createBuiltinProxyForCurrentNetwork(); + if (match != null && matchesCurrentNetwork(match) && match.vlessLocalPort != current.vlessLocalPort) { + switchTo = match; + cfg = match.toVlessConfig(); + } + } if (cfg == null) return; + final VlessConfig startCfg = cfg; + final SharedConfig.ProxyInfo switchToFinal = switchTo; new Thread(() -> { try { Thread.sleep(600); } catch (InterruptedException ignored) {} - Log.d(TAG, "Restarting Xray on new network"); - String err = StartXray(cfg.toJson()); + Log.d(TAG, "Restarting Xray on new network" + (switchToFinal != null ? " (switching server)" : "")); + sCurrentConfig = startCfg; + String err = StartXray(startCfg.toJson()); if (err != null && !err.isEmpty()) { Log.e(TAG, "Xray restart error: " + err); } else { Log.d(TAG, "Xray restarted"); + if (switchToFinal != null) { + SharedConfig.currentProxy = switchToFinal; + persistBuiltinProxySelection(switchToFinal); + ConnectionsManager.setProxySettings( + true, "127.0.0.1", startCfg.localPort, "", "", ""); + } } }, "xray-restart").start(); } @@ -221,6 +302,24 @@ public class XrayController { private static volatile boolean sXrayStartedOnce = false; + /** Persist the given built-in proxy as the active selection in prefs. */ + private static void persistBuiltinProxySelection(SharedConfig.ProxyInfo proxy) { + if (sAppContext == null || proxy == null) return; + try { + int localPort = proxy.vlessLocalPort > 0 ? proxy.vlessLocalPort : DEFAULT_LOCAL_PORT; + sAppContext.getSharedPreferences("mainconfig", Context.MODE_PRIVATE).edit() + .putString("proxy_ip", "127.0.0.1") + .putString("proxy_pass", "") + .putString("proxy_user", "") + .putInt("proxy_port", localPort) + .putString("proxy_secret", "") + .putBoolean("proxy_enabled", true) + .apply(); + } catch (Exception e) { + Log.e(TAG, "persistBuiltinProxySelection failed", e); + } + } + private static void bindToNetwork(ConnectivityManager cm, Network network) { try { // bindProcessToNetwork makes ALL sockets in the process use this network, diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java index ccfd1472..f389e986 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java @@ -41,6 +41,7 @@ import androidx.recyclerview.widget.RecyclerView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.DownloadController; +import org.telegram.messenger.XrayController; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessagesController; import org.telegram.messenger.NotificationCenter; @@ -737,6 +738,16 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente proxyList.clear(); proxyList.addAll(SharedConfig.proxyList); + // Hide built-in proxies that don't match the current network + // (e.g. "WiFi" servers on mobile data and vice versa), but never + // hide the proxy that's currently selected. + for (java.util.Iterator it = proxyList.iterator(); it.hasNext(); ) { + SharedConfig.ProxyInfo info = it.next(); + if (info != SharedConfig.currentProxy && !XrayController.matchesCurrentNetwork(info)) { + it.remove(); + } + } + boolean checking = false; if (!wasCheckedAllList) { for (SharedConfig.ProxyInfo info : proxyList) { @@ -822,6 +833,29 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente continue; } proxyInfo.checking = true; + if (proxyInfo.isVless()) { + // VLESS+Reality servers don't speak the MTProto proxy protocol, + // so Telegram's checkProxy always fails against them. Use a real + // TCP-connect latency probe to the server endpoint instead. + final String host = proxyInfo.address; + final int port = proxyInfo.port; + new Thread(() -> { + long latency = XrayController.measureTcpLatency(host, port, 4000); + AndroidUtilities.runOnUIThread(() -> { + proxyInfo.availableCheckTime = SystemClock.elapsedRealtime(); + proxyInfo.checking = false; + if (latency < 0) { + proxyInfo.available = false; + proxyInfo.ping = 0; + } else { + proxyInfo.ping = latency; + proxyInfo.available = true; + } + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxyCheckDone, proxyInfo); + }); + }, "vless-ping").start(); + continue; + } proxyInfo.proxyCheckPingId = ConnectionsManager.getInstance(currentAccount).checkProxy(proxyInfo.address, proxyInfo.port, proxyInfo.username, proxyInfo.password, proxyInfo.secret, time -> AndroidUtilities.runOnUIThread(() -> { proxyInfo.availableCheckTime = SystemClock.elapsedRealtime(); proxyInfo.checking = false;