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();
|
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;
|
final SharedConfig.ProxyInfo proxyToStart = proxy;
|
||||||
if (proxyToStart == null) return; // no builtin servers configured
|
if (proxyToStart == null) return; // no builtin servers configured
|
||||||
|
SharedConfig.currentProxy = proxyToStart;
|
||||||
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
boolean ok = XrayController.start(proxyToStart.toVlessConfig());
|
boolean ok = XrayController.start(proxyToStart.toVlessConfig());
|
||||||
|
|
@ -448,7 +459,7 @@ public class ApplicationLoader extends Application {
|
||||||
|
|
||||||
private static void autoEnableBuiltinProxy(android.content.SharedPreferences prefs) {
|
private static void autoEnableBuiltinProxy(android.content.SharedPreferences prefs) {
|
||||||
SharedConfig.loadProxyList();
|
SharedConfig.loadProxyList();
|
||||||
SharedConfig.ProxyInfo proxy = XrayController.createBuiltinProxy();
|
SharedConfig.ProxyInfo proxy = XrayController.createBuiltinProxyForCurrentNetwork();
|
||||||
if (proxy == null) {
|
if (proxy == null) {
|
||||||
// No bundled servers configured in this build — mark as handled so we
|
// No bundled servers configured in this build — mark as handled so we
|
||||||
// don't keep probing on every launch.
|
// don't keep probing on every launch.
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import android.util.Log;
|
||||||
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import org.telegram.tgnet.ConnectionsManager;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
|
@ -59,6 +61,63 @@ public class XrayController {
|
||||||
return null;
|
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) {
|
private static SharedConfig.ProxyInfo fromServer(XrayServers.Server s) {
|
||||||
if (s == null || s.address == null || s.address.isEmpty()) return null;
|
if (s == null || s.address == null || s.address.isEmpty()) return null;
|
||||||
SharedConfig.ProxyInfo p = SharedConfig.ProxyInfo.createVless(
|
SharedConfig.ProxyInfo p = SharedConfig.ProxyInfo.createVless(
|
||||||
|
|
@ -184,16 +243,38 @@ public class XrayController {
|
||||||
// Only restart Xray if we actually had a different network before
|
// Only restart Xray if we actually had a different network before
|
||||||
// (skip the initial onAvailable that fires right after registration)
|
// (skip the initial onAvailable that fires right after registration)
|
||||||
if (sXrayStartedOnce) {
|
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;
|
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;
|
if (cfg == null) return;
|
||||||
|
final VlessConfig startCfg = cfg;
|
||||||
|
final SharedConfig.ProxyInfo switchToFinal = switchTo;
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try { Thread.sleep(600); } catch (InterruptedException ignored) {}
|
try { Thread.sleep(600); } catch (InterruptedException ignored) {}
|
||||||
Log.d(TAG, "Restarting Xray on new network");
|
Log.d(TAG, "Restarting Xray on new network" + (switchToFinal != null ? " (switching server)" : ""));
|
||||||
String err = StartXray(cfg.toJson());
|
sCurrentConfig = startCfg;
|
||||||
|
String err = StartXray(startCfg.toJson());
|
||||||
if (err != null && !err.isEmpty()) {
|
if (err != null && !err.isEmpty()) {
|
||||||
Log.e(TAG, "Xray restart error: " + err);
|
Log.e(TAG, "Xray restart error: " + err);
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Xray restarted");
|
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();
|
}, "xray-restart").start();
|
||||||
}
|
}
|
||||||
|
|
@ -221,6 +302,24 @@ public class XrayController {
|
||||||
|
|
||||||
private static volatile boolean sXrayStartedOnce = false;
|
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) {
|
private static void bindToNetwork(ConnectivityManager cm, Network network) {
|
||||||
try {
|
try {
|
||||||
// bindProcessToNetwork makes ALL sockets in the process use this network,
|
// 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.AndroidUtilities;
|
||||||
import org.telegram.messenger.DownloadController;
|
import org.telegram.messenger.DownloadController;
|
||||||
|
import org.telegram.messenger.XrayController;
|
||||||
import org.telegram.messenger.LocaleController;
|
import org.telegram.messenger.LocaleController;
|
||||||
import org.telegram.messenger.MessagesController;
|
import org.telegram.messenger.MessagesController;
|
||||||
import org.telegram.messenger.NotificationCenter;
|
import org.telegram.messenger.NotificationCenter;
|
||||||
|
|
@ -737,6 +738,16 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
|
||||||
proxyList.clear();
|
proxyList.clear();
|
||||||
proxyList.addAll(SharedConfig.proxyList);
|
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;
|
boolean checking = false;
|
||||||
if (!wasCheckedAllList) {
|
if (!wasCheckedAllList) {
|
||||||
for (SharedConfig.ProxyInfo info : proxyList) {
|
for (SharedConfig.ProxyInfo info : proxyList) {
|
||||||
|
|
@ -822,6 +833,29 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
proxyInfo.checking = true;
|
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.proxyCheckPingId = ConnectionsManager.getInstance(currentAccount).checkProxy(proxyInfo.address, proxyInfo.port, proxyInfo.username, proxyInfo.password, proxyInfo.secret, time -> AndroidUtilities.runOnUIThread(() -> {
|
||||||
proxyInfo.availableCheckTime = SystemClock.elapsedRealtime();
|
proxyInfo.availableCheckTime = SystemClock.elapsedRealtime();
|
||||||
proxyInfo.checking = false;
|
proxyInfo.checking = false;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue