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:
parent
bbb2b84f07
commit
becd39da84
3 changed files with 147 additions and 3 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue