mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 18:20:43 +00:00
ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性
被控端 - 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%) - 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败) - 每分钟回头找局域网,自动切回快 400 倍的路径 - 画面心跳帧:静止时省流量,客户端仍知道流是活的 - 锁屏时给出可操作的提示 手表端 - 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException - 连接错误翻译成人话(证书/时间/解析/超时分别提示) - 经中转连上后用服务器报的地址刷新已配对记录 - 新增局域网连接页面、服务器延迟检测 服务器端 - 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502) - 网络调优脚本 net-tune.sh(开机自动应用) - 管理页:批量删除、网络体检、下载区 - 隧道改 HTTP/2 + 双连接 文档 - 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
package com.lanremote.watch;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.security.KeyStore;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
/**
|
||||
* TLS 兼容层:让老手表也能连上中转。
|
||||
*
|
||||
* <p>为什么需要:
|
||||
* 手表端报 {@code SSLHandshakeException: Chain validation failed}。
|
||||
* 中转的证书链是
|
||||
* {@code relay.xxmxxs.top <- GTS WE1 <- GTS Root R4 <- GlobalSign Root CA},
|
||||
* 而很多廉价手表的系统根证书库缺 GTS Root R4(AOSP 2021 年才加),
|
||||
* 于是找不到信任锚,握手直接失败。
|
||||
*
|
||||
* <p>做法:
|
||||
* 把这两个根证书内嵌进 APK,校验时<b>系统库或内置库任一通过即可</b>。
|
||||
* 这两个都是公开的合法根证书,<b>不是跳过校验</b>:
|
||||
* 证书仍必须由可信 CA 签发、且域名匹配,只是把可信 CA 的集合
|
||||
* 从"手表自带的那一份"扩大到"手表自带的 + 这两个"。
|
||||
*
|
||||
* <p>同时把难懂的异常翻译成用户能看懂的话。
|
||||
*/
|
||||
public final class TlsCompat {
|
||||
|
||||
private TlsCompat() {
|
||||
}
|
||||
|
||||
// ---------------- 内置的根证书 ----------------
|
||||
|
||||
/** Google Trust Services Root R4(自签名根,EC P-384)。 */
|
||||
private static final String GTS_ROOT_R4 =
|
||||
"-----BEGIN CERTIFICATE-----\n" +
|
||||
"MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD\n" +
|
||||
"VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG\n" +
|
||||
"A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw\n" +
|
||||
"WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz\n" +
|
||||
"IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi\n" +
|
||||
"AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi\n" +
|
||||
"QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR\n" +
|
||||
"HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\n" +
|
||||
"BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D\n" +
|
||||
"9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8\n" +
|
||||
"p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD\n" +
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
/** GlobalSign Root CA(自签名根,RSA 2048,1998 年起就在各系统里)。 */
|
||||
private static final String GLOBALSIGN_ROOT_CA =
|
||||
"-----BEGIN CERTIFICATE-----\n" +
|
||||
"MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG\n" +
|
||||
"A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv\n" +
|
||||
"b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw\n" +
|
||||
"MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i\n" +
|
||||
"YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT\n" +
|
||||
"aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ\n" +
|
||||
"jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp\n" +
|
||||
"xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp\n" +
|
||||
"1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG\n" +
|
||||
"snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ\n" +
|
||||
"U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8\n" +
|
||||
"9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E\n" +
|
||||
"BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B\n" +
|
||||
"AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz\n" +
|
||||
"yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE\n" +
|
||||
"38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP\n" +
|
||||
"AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad\n" +
|
||||
"DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME\n" +
|
||||
"HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==\n" +
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
private static SSLSocketFactory factory;
|
||||
private static X509TrustManager bundled;
|
||||
private static boolean ready;
|
||||
private static boolean initTried;
|
||||
|
||||
// ---------------- 对外 ----------------
|
||||
|
||||
/**
|
||||
* 打开一个连接,HTTPS 时挂上兼容的 SSLSocketFactory。
|
||||
*
|
||||
* <p>HTTP(局域网中转)不走这里,原样返回。
|
||||
*/
|
||||
public static HttpURLConnection open(String urlText) throws IOException {
|
||||
HttpURLConnection connection =
|
||||
(HttpURLConnection) new URL(urlText).openConnection();
|
||||
connection.setUseCaches(false);
|
||||
if (connection instanceof HttpsURLConnection) {
|
||||
SSLSocketFactory ssl = factory();
|
||||
if (ssl != null) {
|
||||
((HttpsURLConnection) connection).setSSLSocketFactory(ssl);
|
||||
}
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
/** 拿到兼容的 SSLSocketFactory;准备失败就返回 null(退回系统默认)。 */
|
||||
public static synchronized SSLSocketFactory factory() {
|
||||
if (!initTried) {
|
||||
initTried = true;
|
||||
try {
|
||||
KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
store.load(null, null);
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
int index = 0;
|
||||
for (String pem : new String[]{GTS_ROOT_R4, GLOBALSIGN_ROOT_CA}) {
|
||||
X509Certificate cert = (X509Certificate) cf.generateCertificate(
|
||||
new ByteArrayInputStream(pem.getBytes("UTF-8")));
|
||||
store.setCertificateEntry("lanremote-ca-" + (index++), cert);
|
||||
}
|
||||
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
|
||||
TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(store);
|
||||
for (TrustManager tm : tmf.getTrustManagers()) {
|
||||
if (tm instanceof X509TrustManager) {
|
||||
bundled = (X509TrustManager) tm;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
context.init(null, new TrustManager[]{new CompatTrustManager()}, null);
|
||||
factory = context.getSocketFactory();
|
||||
ready = true;
|
||||
} catch (Throwable error) {
|
||||
// 准备失败就退回系统默认,不影响正常设备
|
||||
factory = null;
|
||||
ready = false;
|
||||
}
|
||||
}
|
||||
return ready ? factory : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 TLS / 网络异常翻译成用户能看懂的一句话。
|
||||
*
|
||||
* <p>手表上原本只会显示一长串英文类名,用户完全不知道该怎么办。
|
||||
*/
|
||||
public static String explain(Throwable error) {
|
||||
if (error == null) {
|
||||
return "未知错误";
|
||||
}
|
||||
String text = String.valueOf(error);
|
||||
String lower = text.toLowerCase();
|
||||
|
||||
if (lower.contains("chain validation failed")
|
||||
|| lower.contains("trust anchor")
|
||||
|| lower.contains("certpathvalidator")) {
|
||||
return "证书校验失败(中转的根证书手表不认识)。"
|
||||
+ "可能是手表系统太旧,或网络被劫持。";
|
||||
}
|
||||
if (lower.contains("not yet valid") || lower.contains("expired")
|
||||
|| lower.contains("certificate expired")) {
|
||||
return "证书时间不对 —— 把手表的【日期和时间】调成自动获取,"
|
||||
+ "再试一次。";
|
||||
}
|
||||
if (lower.contains("timeout") || lower.contains("timed out")) {
|
||||
return "连接超时(网络太慢或被挡住)";
|
||||
}
|
||||
if (lower.contains("unable to resolve host")
|
||||
|| lower.contains("unknownhost")) {
|
||||
return "域名解析不了(检查手表能不能上网)";
|
||||
}
|
||||
if (lower.contains("connection refused")) {
|
||||
return "对方拒绝了连接(电脑上 ykxmz 没开?)";
|
||||
}
|
||||
if (lower.contains("network is unreachable")
|
||||
|| lower.contains("no route")) {
|
||||
return "手表没联网(检查 WiFi)";
|
||||
}
|
||||
if (lower.contains("eof") || lower.contains("reset")) {
|
||||
return "连接被中断(网络不稳)";
|
||||
}
|
||||
if (text.length() > 80) {
|
||||
return text.substring(0, 80);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组合信任:系统默认 或 内置根证书,任一通过即认可。
|
||||
*
|
||||
* <p>这是修这个问题的关键 ——
|
||||
* 老手表缺 GTS Root R4,但内置的那份有,于是校验通过;
|
||||
* 新设备走系统默认照样通过。两边都不削弱安全性。
|
||||
*/
|
||||
private static final class CompatTrustManager implements X509TrustManager {
|
||||
|
||||
private final X509TrustManager system = findSystem();
|
||||
|
||||
private static X509TrustManager findSystem() {
|
||||
try {
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
|
||||
TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init((KeyStore) null); // null = 系统默认
|
||||
for (TrustManager tm : tmf.getTrustManagers()) {
|
||||
if (tm instanceof X509TrustManager) {
|
||||
return (X509TrustManager) tm;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 拿不到就算了,下面用内置的
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
||||
throws java.security.cert.CertificateException {
|
||||
if (system != null) {
|
||||
system.checkClientTrusted(chain, authType);
|
||||
return;
|
||||
}
|
||||
if (bundled != null) {
|
||||
bundled.checkClientTrusted(chain, authType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
||||
throws java.security.cert.CertificateException {
|
||||
java.security.cert.CertificateException first = null;
|
||||
if (system != null) {
|
||||
try {
|
||||
system.checkServerTrusted(chain, authType);
|
||||
return;
|
||||
} catch (java.security.cert.CertificateException error) {
|
||||
first = error;
|
||||
}
|
||||
}
|
||||
if (bundled != null) {
|
||||
try {
|
||||
bundled.checkServerTrusted(chain, authType);
|
||||
return;
|
||||
} catch (java.security.cert.CertificateException error) {
|
||||
if (first == null) {
|
||||
first = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw (first != null ? first
|
||||
: new java.security.cert.CertificateException(
|
||||
"证书校验失败"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
X509Certificate[] a = system == null
|
||||
? new X509Certificate[0] : system.getAcceptedIssuers();
|
||||
X509Certificate[] b = bundled == null
|
||||
? new X509Certificate[0] : bundled.getAcceptedIssuers();
|
||||
X509Certificate[] all = new X509Certificate[a.length + b.length];
|
||||
System.arraycopy(a, 0, all, 0, a.length);
|
||||
System.arraycopy(b, 0, all, a.length, b.length);
|
||||
return all;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user