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
This commit is contained in:
instant992 2026-06-10 23:56:31 +04:00
parent bbb2b84f07
commit becd39da84
3 changed files with 147 additions and 3 deletions

View file

@ -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.

View file

@ -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,

View file

@ -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<SharedConfig.ProxyInfo> 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;