feat: 1.新增在 VPN 配置中添加了一条规则,2.修复全局代理无法使用
This commit is contained in:
parent
59aa67d456
commit
679c303457
@ -2116,7 +2116,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
kr_isConnected.value = true;
|
kr_isConnected.value = true;
|
||||||
kr_startConnectionTimer();
|
kr_startConnectionTimer();
|
||||||
kr_updateConnectionInfo();
|
kr_updateConnectionInfo();
|
||||||
KRSingBoxImp.instance.kr_debugTunConnectivity();
|
|
||||||
|
|
||||||
// 🔧 修复:同步已启动状态时,尝试更新延迟值
|
// 🔧 修复:同步已启动状态时,尝试更新延迟值
|
||||||
if (!_kr_tryUpdateDelayFromActiveGroups()) {
|
if (!_kr_tryUpdateDelayFromActiveGroups()) {
|
||||||
|
|||||||
@ -16,16 +16,14 @@ import 'package:path/path.dart' as p;
|
|||||||
import '../../../core/model/directories.dart';
|
import '../../../core/model/directories.dart';
|
||||||
import '../../../singbox/model/singbox_config_option.dart';
|
import '../../../singbox/model/singbox_config_option.dart';
|
||||||
import '../../../singbox/model/singbox_outbound.dart';
|
import '../../../singbox/model/singbox_outbound.dart';
|
||||||
import '../../../singbox/model/singbox_proxy_type.dart';
|
|
||||||
import '../../../singbox/model/singbox_stats.dart';
|
import '../../../singbox/model/singbox_stats.dart';
|
||||||
import '../../../singbox/model/singbox_status.dart';
|
import '../../../singbox/model/singbox_status.dart';
|
||||||
import '../../utils/kr_country_util.dart';
|
import '../../utils/kr_country_util.dart';
|
||||||
import '../../utils/kr_log_util.dart';
|
import '../../utils/kr_log_util.dart';
|
||||||
import '../../utils/kr_secure_storage.dart';
|
import '../../utils/kr_secure_storage.dart';
|
||||||
|
import '../../utils/kr_windows_dns_util.dart';
|
||||||
import '../../common/app_run_data.dart';
|
import '../../common/app_run_data.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
||||||
|
|
||||||
|
|
||||||
enum KRConnectionType {
|
enum KRConnectionType {
|
||||||
global,
|
global,
|
||||||
@ -427,6 +425,12 @@ class KRSingBoxImp {
|
|||||||
);
|
);
|
||||||
|
|
||||||
KRLogUtil.kr_i('✅ SingBox 初始化完成');
|
KRLogUtil.kr_i('✅ SingBox 初始化完成');
|
||||||
|
|
||||||
|
// 🔧 提取 geosite 文件到 workingDir
|
||||||
|
KRLogUtil.kr_i('📦 开始提取 geosite 文件...', tag: 'SingBox');
|
||||||
|
await _kr_extractGeositeFiles();
|
||||||
|
KRLogUtil.kr_i('✅ geosite 文件提取完成', tag: 'SingBox');
|
||||||
|
|
||||||
_kr_isInitialized = true;
|
_kr_isInitialized = true;
|
||||||
|
|
||||||
// 🔑 关键:在初始化完成后立即订阅状态变化流
|
// 🔑 关键:在初始化完成后立即订阅状态变化流
|
||||||
@ -481,22 +485,78 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 提取 geosite 文件到 workingDir
|
||||||
|
Future<void> _kr_extractGeositeFiles() async {
|
||||||
|
try {
|
||||||
|
// 创建 geosite 目录
|
||||||
|
final geositeDir = Directory(p.join(kr_configDics.workingDir.path, 'geosite'));
|
||||||
|
if (!geositeDir.existsSync()) {
|
||||||
|
await geositeDir.create(recursive: true);
|
||||||
|
KRLogUtil.kr_i('✅ 已创建 geosite 目录: ${geositeDir.path}', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要提取的文件列表
|
||||||
|
final files = ['geoip-cn.srs', 'geosite-cn.srs'];
|
||||||
|
|
||||||
|
for (final filename in files) {
|
||||||
|
final assetPath = 'assets/geosite/$filename';
|
||||||
|
final targetPath = p.join(geositeDir.path, filename);
|
||||||
|
final targetFile = File(targetPath);
|
||||||
|
|
||||||
|
// 检查文件是否已存在
|
||||||
|
if (targetFile.existsSync()) {
|
||||||
|
final fileSize = await targetFile.length();
|
||||||
|
KRLogUtil.kr_i('📄 $filename 已存在 (${fileSize} bytes), 跳过', tag: 'SingBox');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 assets 加载文件
|
||||||
|
KRLogUtil.kr_i('📥 正在提取 $filename...', tag: 'SingBox');
|
||||||
|
final byteData = await rootBundle.load(assetPath);
|
||||||
|
final bytes = byteData.buffer.asUint8List();
|
||||||
|
|
||||||
|
// 写入文件系统
|
||||||
|
await targetFile.writeAsBytes(bytes);
|
||||||
|
final writtenSize = await targetFile.length();
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ 提取成功: $filename (${writtenSize} bytes)', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('🎉 所有 geosite 文件提取完成', tag: 'SingBox');
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
KRLogUtil.kr_e('❌ 提取 geosite 文件失败: $e', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_e('堆栈: $stackTrace', tag: 'SingBox');
|
||||||
|
// 不抛出异常,让应用继续运行(使用远程规则集作为后备)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _getConfigOption() {
|
Map<String, dynamic> _getConfigOption() {
|
||||||
// 不使用缓存,每次都重新生成配置
|
// 不使用缓存,每次都重新生成配置
|
||||||
// if (kr_configOption.isNotEmpty) {
|
// if (kr_configOption.isNotEmpty) {
|
||||||
// return kr_configOption;
|
// return kr_configOption;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// 🔧 关键修复:全局代理模式下,region 强制设为 'other',libcore 就不会生成国家直连规则
|
||||||
|
final String effectiveRegion;
|
||||||
|
if (kr_connectionType.value == KRConnectionType.global) {
|
||||||
|
effectiveRegion = 'other'; // 全局代理:不添加任何国家规则
|
||||||
|
KRLogUtil.kr_i('🌐 [全局代理模式] region 设为 other,所有流量走代理', tag: 'SingBox');
|
||||||
|
} else {
|
||||||
|
effectiveRegion = KRCountryUtil.kr_getCurrentCountryCode(); // 智能代理:使用用户选择的国家
|
||||||
|
KRLogUtil.kr_i('✅ [智能代理模式] region 设为 $effectiveRegion', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
|
||||||
final op = {
|
final op = {
|
||||||
"region": "other", // 参考 hiddify-app: 默认使用 "other" 跳过规则集下载
|
"region": effectiveRegion, // 🔧 修复:根据出站模式动态设置 region
|
||||||
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
|
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
|
||||||
"use-xray-core-when-possible": false,
|
"use-xray-core-when-possible": false,
|
||||||
"execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写
|
"execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写
|
||||||
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
|
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
|
||||||
"resolve-destination": false,
|
"resolve-destination": false,
|
||||||
"ipv6-mode": "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only)
|
"ipv6-mode": "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only)
|
||||||
"remote-dns-address": "https://dns.google/dns-query", // 使用 Google DoH,避免中转节点 DNS 死锁
|
"remote-dns-address": "https://dns.google/dns-query", // 使用 Google DoH,避免中转节点 DNS 死锁
|
||||||
"remote-dns-domain-strategy": "prefer_ipv4",
|
"remote-dns-domain-strategy": "prefer_ipv4",
|
||||||
"direct-dns-address": "local" ,
|
"direct-dns-address": "local", // 使用系统 DNS,确保中转服务器域名能被解析
|
||||||
"direct-dns-domain-strategy": "prefer_ipv4",
|
"direct-dns-domain-strategy": "prefer_ipv4",
|
||||||
"mixed-port": kr_port,
|
"mixed-port": kr_port,
|
||||||
"tproxy-port": kr_port,
|
"tproxy-port": kr_port,
|
||||||
@ -511,13 +571,21 @@ class KRSingBoxImp {
|
|||||||
"enable-tun": Platform.isIOS || Platform.isAndroid,
|
"enable-tun": Platform.isIOS || Platform.isAndroid,
|
||||||
"enable-tun-service": false,
|
"enable-tun-service": false,
|
||||||
"set-system-proxy":
|
"set-system-proxy":
|
||||||
Platform.isWindows || Platform.isLinux || Platform.isMacOS,
|
Platform.isWindows || Platform.isLinux || Platform.isMacOS,
|
||||||
"bypass-lan": false,
|
"bypass-lan": false,
|
||||||
"allow-connection-from-lan": false,
|
"allow-connection-from-lan": false,
|
||||||
"enable-fake-dns": false,
|
"enable-fake-dns": false,
|
||||||
"enable-dns-routing": true,
|
"enable-dns-routing": true,
|
||||||
"independent-dns-cache": true,
|
"independent-dns-cache": true,
|
||||||
"rules": [],
|
"rules": [
|
||||||
|
// ✅ 自定义域名直连规则 - 添加到 HiddifyOptions.Rules 中
|
||||||
|
// 这样 Native 层的 config.BuildConfig() 会将其包含到最终配置
|
||||||
|
// 注意:libcore 要求 domains 字段使用前缀格式,如 "domain:ip.sb" 表示 domain_suffix
|
||||||
|
{
|
||||||
|
"domains": "domain:api.hifast.biz", // domain: 前缀表示 domain_suffix 匹配
|
||||||
|
"outbound": "bypass" // bypass = direct
|
||||||
|
}
|
||||||
|
],
|
||||||
"mux": {
|
"mux": {
|
||||||
"enable": false,
|
"enable": false,
|
||||||
"padding": false,
|
"padding": false,
|
||||||
@ -562,6 +630,17 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
kr_configOption = op;
|
kr_configOption = op;
|
||||||
|
|
||||||
|
// 🔧 调试日志:确认自定义规则已添加
|
||||||
|
final rules = op["rules"] as List?;
|
||||||
|
KRLogUtil.kr_i('✅ HiddifyOptions 已生成,包含 ${rules?.length ?? 0} 条自定义路由规则', tag: 'SingBox');
|
||||||
|
if (rules != null && rules.isNotEmpty) {
|
||||||
|
for (var rule in rules) {
|
||||||
|
final ruleMap = rule as Map<String, dynamic>;
|
||||||
|
KRLogUtil.kr_i(' - 规则: domains=${ruleMap["domains"]}, outbound=${ruleMap["outbound"]}', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return op;
|
return op;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1025,25 +1104,10 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_i('💾 开始保存配置文件...', tag: 'SingBox');
|
KRLogUtil.kr_i('💾 开始保存配置文件...', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📊 出站节点数量: ${outbounds.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('📊 出站节点数量: ${outbounds.length}', tag: 'SingBox');
|
||||||
|
|
||||||
// 分离真实节点和自动选择节点
|
|
||||||
final realNodes = <Map<String, dynamic>>[];
|
|
||||||
final autoNodes = <Map<String, dynamic>>[];
|
|
||||||
|
|
||||||
for (final outbound in outbounds) {
|
|
||||||
if (outbound['tag']?.toString().endsWith('-auto') == true) {
|
|
||||||
autoNodes.add(outbound);
|
|
||||||
} else {
|
|
||||||
realNodes.add(outbound);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('📊 真实节点数量: ${realNodes.length}', tag: 'SingBox');
|
|
||||||
KRLogUtil.kr_i('📊 自动选择节点数量: ${autoNodes.length}', tag: 'SingBox');
|
|
||||||
|
|
||||||
// 打印每个节点的详细配置
|
// 打印每个节点的详细配置
|
||||||
for (int i = 0; i < realNodes.length; i++) {
|
for (int i = 0; i < outbounds.length; i++) {
|
||||||
final outbound = realNodes[i];
|
final outbound = outbounds[i];
|
||||||
KRLogUtil.kr_i('📋 真实节点[$i] 配置:', tag: 'SingBox');
|
KRLogUtil.kr_i('📋 节点[$i] 配置:', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox');
|
KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox');
|
KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i(' - server: ${outbound['server']}', tag: 'SingBox');
|
KRLogUtil.kr_i(' - server: ${outbound['server']}', tag: 'SingBox');
|
||||||
@ -1051,26 +1115,20 @@ class KRSingBoxImp {
|
|||||||
if (outbound['method'] != null) {
|
if (outbound['method'] != null) {
|
||||||
KRLogUtil.kr_i(' - method: ${outbound['method']}', tag: 'SingBox');
|
KRLogUtil.kr_i(' - method: ${outbound['method']}', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
|
if (outbound['interval'] != null) {
|
||||||
|
KRLogUtil.kr_i(' - interval: ${outbound['interval']}', tag: 'SingBox');
|
||||||
|
}
|
||||||
if (outbound['password'] != null) {
|
if (outbound['password'] != null) {
|
||||||
KRLogUtil.kr_i(' - password: ${outbound['password']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
KRLogUtil.kr_i(' - password: ${outbound['password']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
if (outbound['uuid'] != null) {
|
if (outbound['uuid'] != null) {
|
||||||
KRLogUtil.kr_i(' - uuid: ${outbound['uuid']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
KRLogUtil.kr_i(' - uuid: ${outbound['uuid']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
}
|
KRLogUtil.kr_i(' - 完整配置: ${jsonEncode(outbound)}', tag: 'SingBox');
|
||||||
|
|
||||||
for (int i = 0; i < autoNodes.length; i++) {
|
|
||||||
final outbound = autoNodes[i];
|
|
||||||
KRLogUtil.kr_i('🤖 自动选择节点[$i] 配置:', tag: 'SingBox');
|
|
||||||
KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox');
|
|
||||||
KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox');
|
|
||||||
if (outbound['outbounds'] != null) {
|
|
||||||
KRLogUtil.kr_i(' - outbounds: ${outbound['outbounds']}', tag: 'SingBox');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⚠️ 临时过滤 Hysteria2 节点以避免 libcore 崩溃
|
// ⚠️ 临时过滤 Hysteria2 节点以避免 libcore 崩溃
|
||||||
final filteredRealNodes = realNodes.where((outbound) {
|
kr_outbounds = outbounds.where((outbound) {
|
||||||
final type = outbound['type'];
|
final type = outbound['type'];
|
||||||
if (type == 'hysteria2' || type == 'hysteria') {
|
if (type == 'hysteria2' || type == 'hysteria') {
|
||||||
KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox');
|
KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox');
|
||||||
@ -1079,30 +1137,11 @@ class KRSingBoxImp {
|
|||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
// 过滤自动选择节点中的 Hysteria2
|
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
|
||||||
final filteredAutoNodes = autoNodes.where((outbound) {
|
|
||||||
if (outbound['outbounds'] != null) {
|
|
||||||
final outbounds = outbound['outbounds'] as List<dynamic>;
|
|
||||||
// 检查是否包含 Hysteria2 节点
|
|
||||||
final hasHysteria2 = outbounds.any((tag) => tag.toString().toLowerCase().contains('hysteria'));
|
|
||||||
if (hasHysteria2) {
|
|
||||||
KRLogUtil.kr_w('⚠️ 跳过包含 Hysteria2 的自动选择节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('✅ 过滤后真实节点数量: ${filteredRealNodes.length}/${realNodes.length}', tag: 'SingBox');
|
|
||||||
KRLogUtil.kr_i('✅ 过滤后自动选择节点数量: ${filteredAutoNodes.length}/${autoNodes.length}', tag: 'SingBox');
|
|
||||||
|
|
||||||
// 构建所有可用标签列表(selector 包含真实和自动节点)
|
|
||||||
final allAvailableTags = <String>[];
|
|
||||||
allAvailableTags.addAll(filteredAutoNodes.map((o) => o['tag'] as String));
|
|
||||||
allAvailableTags.addAll(filteredRealNodes.map((o) => o['tag'] as String));
|
|
||||||
|
|
||||||
// 🔧 修复:生成完整的 SingBox 配置
|
// 🔧 修复:生成完整的 SingBox 配置
|
||||||
// 现在保存完整配置,包含所有必需字段,并且 outbounds 顺序为:selector→自动组→真实节点→direct/block/dns-out
|
// 之前只保存 {"outbounds": [...]}, 导致 libcore 无法正确处理
|
||||||
|
// 现在保存完整配置,包含所有必需字段
|
||||||
final Map<String, dynamic> fullConfig = {
|
final Map<String, dynamic> fullConfig = {
|
||||||
"log": {
|
"log": {
|
||||||
"level": "debug",
|
"level": "debug",
|
||||||
@ -1112,8 +1151,7 @@ class KRSingBoxImp {
|
|||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"tag": "dns-remote",
|
"tag": "dns-remote",
|
||||||
"address": "https://dns.google/dns-query",
|
"address": "https://1.1.1.1/dns-query",
|
||||||
"detour": "direct",
|
|
||||||
"address_resolver": "dns-direct"
|
"address_resolver": "dns-direct"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1122,7 +1160,7 @@ class KRSingBoxImp {
|
|||||||
"detour": "direct"
|
"detour": "direct"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rules": [],
|
"rules": _kr_buildDnsRules(), // ✅ 使用动态构建的 DNS 规则
|
||||||
"final": "dns-remote",
|
"final": "dns-remote",
|
||||||
"strategy": "prefer_ipv4"
|
"strategy": "prefer_ipv4"
|
||||||
},
|
},
|
||||||
@ -1139,17 +1177,14 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outbounds": [
|
"outbounds": [
|
||||||
// 🔧 修复:添加 selector 组,包含真实和自动节点标签
|
// 🔧 修复:添加 selector 组,让用户可以手动选择节点
|
||||||
{
|
{
|
||||||
"type": "selector",
|
"type": "selector",
|
||||||
"tag": "proxy",
|
"tag": "proxy",
|
||||||
"outbounds": allAvailableTags,
|
"outbounds": kr_outbounds.map((o) => o['tag'] as String).toList(),
|
||||||
"default": allAvailableTags.isNotEmpty ? allAvailableTags[0] : "direct",
|
"default": kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct",
|
||||||
},
|
},
|
||||||
// 🔧 修复:自动选择组排在真实节点前面
|
...kr_outbounds,
|
||||||
...filteredAutoNodes,
|
|
||||||
// 🔧 修复:真实节点排在后面
|
|
||||||
...filteredRealNodes,
|
|
||||||
{
|
{
|
||||||
"type": "direct",
|
"type": "direct",
|
||||||
"tag": "direct"
|
"tag": "direct"
|
||||||
@ -1164,13 +1199,8 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"route": {
|
"route": {
|
||||||
"rules": [
|
"rules": _kr_buildRouteRules(), // ✅ 使用动态构建的路由规则
|
||||||
{ "type": "dns", "outbound": "dns-out" }, // sing-box 内部解析交给 dns-out
|
"rule_set": _kr_buildRuleSets(), // ✅ 使用动态构建的规则集
|
||||||
{ "type": "field", "port": 53, "network": "udp", "outbound": "direct" }, // 系统 UDP 53 直连
|
|
||||||
{ "type": "field", "port": 53, "network": "tcp", "outbound": "direct" }, // 系统 TCP 53 直连
|
|
||||||
{ "type": "field", "port": 853, "network": "tcp", "outbound": "direct" }, // 系统 TCP 853(Private DNS)直连
|
|
||||||
{ "type": "field", "domain": ["dns.google", "cloudflare-dns.com", "one.one.one.one"], "outbound": "direct" } // 常见 DoH 域名直连,避免误走代理
|
|
||||||
],
|
|
||||||
"final": "proxy", // 🔧 修复:使用 selector 组作为默认出站
|
"final": "proxy", // 🔧 修复:使用 selector 组作为默认出站
|
||||||
"auto_detect_interface": true
|
"auto_detect_interface": true
|
||||||
}
|
}
|
||||||
@ -1181,8 +1211,7 @@ class KRSingBoxImp {
|
|||||||
final mapStr = jsonEncode(fullConfig);
|
final mapStr = jsonEncode(fullConfig);
|
||||||
|
|
||||||
KRLogUtil.kr_i('📄 完整配置文件长度: ${mapStr.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('📄 完整配置文件长度: ${mapStr.length}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📄 Outbounds 总数: ${filteredAutoNodes.length + filteredRealNodes.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('📄 Outbounds 数量: ${kr_outbounds.length}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📄 Selector 可用标签数: ${allAvailableTags.length}', tag: 'SingBox');
|
|
||||||
KRLogUtil.kr_i('📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}', tag: 'SingBox');
|
KRLogUtil.kr_i('📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}', tag: 'SingBox');
|
||||||
|
|
||||||
await file.writeAsString(mapStr);
|
await file.writeAsString(mapStr);
|
||||||
@ -1200,6 +1229,128 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_i('✅ 配置文件保存完成', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ 配置文件保存完成', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 构建 DNS 规则
|
||||||
|
List<Map<String, dynamic>> _kr_buildDnsRules() {
|
||||||
|
final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode();
|
||||||
|
final rules = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
// 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则
|
||||||
|
// 如果是"全局代理"模式,即使选择了国家也不添加直连规则
|
||||||
|
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
|
||||||
|
rules.add({
|
||||||
|
"rule_set": [
|
||||||
|
"geoip-$currentCountryCode",
|
||||||
|
"geosite-$currentCountryCode",
|
||||||
|
],
|
||||||
|
"server": "dns-direct"
|
||||||
|
});
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ [智能代理模式] 添加 DNS 规则: $currentCountryCode 域名使用直连 DNS', tag: 'SingBox');
|
||||||
|
} else if (kr_connectionType.value == KRConnectionType.global) {
|
||||||
|
KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家 DNS 规则,所有DNS查询走代理', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建路由规则
|
||||||
|
List<Map<String, dynamic>> _kr_buildRouteRules() {
|
||||||
|
final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode();
|
||||||
|
final rules = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
// 基础规则: DNS 查询走 dns-out
|
||||||
|
rules.add({
|
||||||
|
"protocol": "dns",
|
||||||
|
"outbound": "dns-out"
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ 自定义域名直连规则(优先级高,放在前面)
|
||||||
|
// rules.add({
|
||||||
|
// "domain_suffix": ["ip138.com"],
|
||||||
|
// "outbound": "direct"
|
||||||
|
// });
|
||||||
|
|
||||||
|
// KRLogUtil.kr_i('✅ 添加自定义域名直连规则: ip138.com -> direct', tag: 'SingBox');
|
||||||
|
|
||||||
|
// 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则
|
||||||
|
// 如果是"全局代理"模式,即使选择了国家也不添加直连规则
|
||||||
|
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
|
||||||
|
rules.add({
|
||||||
|
"rule_set": [
|
||||||
|
"geoip-$currentCountryCode",
|
||||||
|
"geosite-$currentCountryCode",
|
||||||
|
],
|
||||||
|
"outbound": "direct"
|
||||||
|
});
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ [智能代理模式] 添加路由规则: $currentCountryCode IP/域名直连', tag: 'SingBox');
|
||||||
|
} else if (kr_connectionType.value == KRConnectionType.global) {
|
||||||
|
KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家路由规则,所有流量走代理', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建规则集配置
|
||||||
|
List<Map<String, dynamic>> _kr_buildRuleSets() {
|
||||||
|
final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode();
|
||||||
|
final ruleSets = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
// 🔧 关键修复:只有在"智能代理"模式下,才加载国家规则集
|
||||||
|
// 如果是"全局代理"模式,即使选择了国家也不加载规则集
|
||||||
|
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
|
||||||
|
// 检查本地文件是否存在
|
||||||
|
final geoipFile = File(p.join(kr_configDics.workingDir.path, 'geosite', 'geoip-$currentCountryCode.srs'));
|
||||||
|
final geositeFile = File(p.join(kr_configDics.workingDir.path, 'geosite', 'geosite-$currentCountryCode.srs'));
|
||||||
|
|
||||||
|
if (geoipFile.existsSync() && geositeFile.existsSync()) {
|
||||||
|
// ✅ 使用本地文件
|
||||||
|
ruleSets.add({
|
||||||
|
"type": "local",
|
||||||
|
"tag": "geoip-$currentCountryCode",
|
||||||
|
"format": "binary",
|
||||||
|
"path": "./geosite/geoip-$currentCountryCode.srs" // 相对于 workingDir
|
||||||
|
});
|
||||||
|
|
||||||
|
ruleSets.add({
|
||||||
|
"type": "local",
|
||||||
|
"tag": "geosite-$currentCountryCode",
|
||||||
|
"format": "binary",
|
||||||
|
"path": "./geosite/geosite-$currentCountryCode.srs"
|
||||||
|
});
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ 使用本地规则集: $currentCountryCode', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_i(' - geoip: ./geosite/geoip-$currentCountryCode.srs', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_i(' - geosite: ./geosite/geosite-$currentCountryCode.srs', tag: 'SingBox');
|
||||||
|
} else {
|
||||||
|
// ❌ 本地文件不存在,使用远程规则集作为后备
|
||||||
|
KRLogUtil.kr_w('⚠️ 本地规则集不存在,使用远程规则集', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_w(' - geoip 文件存在: ${geoipFile.existsSync()}', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_w(' - geosite 文件存在: ${geositeFile.existsSync()}', tag: 'SingBox');
|
||||||
|
|
||||||
|
ruleSets.add({
|
||||||
|
"type": "remote",
|
||||||
|
"tag": "geoip-$currentCountryCode",
|
||||||
|
"format": "binary",
|
||||||
|
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geoip-$currentCountryCode.srs",
|
||||||
|
"download_detour": "direct",
|
||||||
|
"update_interval": "7d"
|
||||||
|
});
|
||||||
|
|
||||||
|
ruleSets.add({
|
||||||
|
"type": "remote",
|
||||||
|
"tag": "geosite-$currentCountryCode",
|
||||||
|
"format": "binary",
|
||||||
|
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geosite-$currentCountryCode.srs",
|
||||||
|
"download_detour": "direct",
|
||||||
|
"update_interval": "7d"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ruleSets;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> kr_start() async {
|
Future<void> kr_start() async {
|
||||||
// 不再手动设置状态,libcore 会通过 status stream 自动发送状态更新
|
// 不再手动设置状态,libcore 会通过 status stream 自动发送状态更新
|
||||||
try {
|
try {
|
||||||
@ -1211,9 +1362,33 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
// ⚠️ 强制编译标记 - v2.0-lazy-load
|
// ⚠️ 强制编译标记 - v2.0-lazy-load
|
||||||
KRLogUtil.kr_i('🚀🚀🚀 [v2.0-lazy-load] 开始启动 SingBox...', tag: 'SingBox');
|
KRLogUtil.kr_i('🚀🚀🚀 [v2.0-lazy-load] 开始启动 SingBox...', tag: 'SingBox');
|
||||||
|
|
||||||
|
// 🔧 强制重新生成配置文件(确保最新的路由规则生效)
|
||||||
|
if (kr_outbounds.isNotEmpty) {
|
||||||
|
KRLogUtil.kr_i('🔄 启动前强制重新生成配置文件...', tag: 'SingBox');
|
||||||
|
kr_saveOutbounds(kr_outbounds);
|
||||||
|
// 等待配置文件写入完成
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
|
||||||
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
|
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📝 配置名称: $kr_configName', tag: 'SingBox');
|
KRLogUtil.kr_i('📝 配置名称: $kr_configName', tag: 'SingBox');
|
||||||
|
|
||||||
|
// 🔑 Windows 平台:在启动前备份 DNS 设置
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
KRLogUtil.kr_i('🪟 Windows 平台,备份 DNS 设置...', tag: 'SingBox');
|
||||||
|
try {
|
||||||
|
final backupSuccess = await KRWindowsDnsUtil.instance.kr_backupDnsSettings();
|
||||||
|
if (backupSuccess) {
|
||||||
|
KRLogUtil.kr_i('✅ Windows DNS 备份成功', tag: 'SingBox');
|
||||||
|
} else {
|
||||||
|
KRLogUtil.kr_w('⚠️ Windows DNS 备份失败,将在停止时使用兜底恢复', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_w('⚠️ Windows DNS 备份异常: $e,将在停止时使用兜底恢复', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🔑 先尝试停止旧实例,避免 command.sock 冲突
|
// 🔑 先尝试停止旧实例,避免 command.sock 冲突
|
||||||
// 注意:如果没有旧实例,libcore 会报错 "command.sock: no such file",这是正常的
|
// 注意:如果没有旧实例,libcore 会报错 "command.sock: no such file",这是正常的
|
||||||
try {
|
try {
|
||||||
@ -1367,12 +1542,12 @@ class KRSingBoxImp {
|
|||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
// 添加超时保护,防止 stop() 调用阻塞
|
// 添加超时保护,防止 stop() 调用阻塞
|
||||||
// 缩短超时时间,避免用户等待过久
|
// 🔧 延长超时时间到 10 秒,给 Windows DNS 清理足够时间
|
||||||
try {
|
try {
|
||||||
await kr_singBox.stop().run().timeout(
|
await kr_singBox.stop().run().timeout(
|
||||||
const Duration(seconds: 3),
|
const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒
|
||||||
onTimeout: () {
|
onTimeout: () {
|
||||||
KRLogUtil.kr_w('⚠️ 停止操作超时(3秒),强制继续', tag: 'SingBox');
|
KRLogUtil.kr_w('⚠️ 停止操作超时(10秒),强制继续', tag: 'SingBox');
|
||||||
return const Left('timeout');
|
return const Left('timeout');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -1381,7 +1556,29 @@ class KRSingBoxImp {
|
|||||||
// 继续执行清理操作
|
// 继续执行清理操作
|
||||||
}
|
}
|
||||||
|
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
// 🔑 Windows 平台:停止后立即恢复 DNS 设置
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
KRLogUtil.kr_i('🪟 Windows 平台,开始恢复 DNS 设置...', tag: 'SingBox');
|
||||||
|
|
||||||
|
// 等待 sing-box 完全停止
|
||||||
|
await Future.delayed(const Duration(milliseconds: 1000));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 尝试恢复 DNS
|
||||||
|
final restoreSuccess = await KRWindowsDnsUtil.instance.kr_restoreDnsSettings();
|
||||||
|
if (restoreSuccess) {
|
||||||
|
KRLogUtil.kr_i('✅ Windows DNS 恢复成功', tag: 'SingBox');
|
||||||
|
} else {
|
||||||
|
KRLogUtil.kr_e('❌ Windows DNS 恢复失败', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ Windows DNS 恢复异常: $e', tag: 'SingBox');
|
||||||
|
// 异常时也会在工具类内部执行兜底恢复
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 非 Windows 平台正常等待
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
|
|
||||||
// 取消统计和分组订阅,但保留状态订阅以便继续接收状态更新
|
// 取消统计和分组订阅,但保留状态订阅以便继续接收状态更新
|
||||||
final subscriptionsToCancel = _kr_subscriptions.where((sub) {
|
final subscriptionsToCancel = _kr_subscriptions.where((sub) {
|
||||||
@ -1403,6 +1600,17 @@ class KRSingBoxImp {
|
|||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
KRLogUtil.kr_e('停止服务时出错: $e');
|
KRLogUtil.kr_e('停止服务时出错: $e');
|
||||||
KRLogUtil.kr_e('错误堆栈: $stackTrace');
|
KRLogUtil.kr_e('错误堆栈: $stackTrace');
|
||||||
|
|
||||||
|
// 🔑 即使出错,也要尝试恢复 Windows DNS
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 停止异常,强制执行 DNS 恢复', tag: 'SingBox');
|
||||||
|
try {
|
||||||
|
await KRWindowsDnsUtil.instance.kr_restoreDnsSettings();
|
||||||
|
} catch (dnsError) {
|
||||||
|
KRLogUtil.kr_e('❌ 强制 DNS 恢复失败: $dnsError', tag: 'SingBox');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 不手动设置状态,信任 libcore 的状态管理
|
// 不手动设置状态,信任 libcore 的状态管理
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@ -1426,10 +1634,25 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> kr_restart() async {
|
Future<void> kr_restart() async {
|
||||||
KRLogUtil.kr_i("restart");
|
KRLogUtil.kr_i('🔄 重启 SingBox...', tag: 'SingBox');
|
||||||
kr_singBox.restart(_cutPath, kr_configName, false).mapLeft((err) {
|
|
||||||
KRLogUtil.kr_e('重启失败: $err');
|
// 🔧 修复:使用 stop + start 而不是 restart
|
||||||
}).run();
|
// 这样可以触发 Windows DNS 恢复和备份逻辑
|
||||||
|
try {
|
||||||
|
// 1. 先停止(会触发 DNS 恢复)
|
||||||
|
await kr_stop();
|
||||||
|
|
||||||
|
// 2. 等待完全停止
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
// 3. 重新启动(会触发 DNS 备份)
|
||||||
|
await kr_start();
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ SingBox 重启完成', tag: 'SingBox');
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ 重启失败: $e', tag: 'SingBox');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//// 设置出站模式
|
//// 设置出站模式
|
||||||
@ -1571,9 +1794,11 @@ class KRSingBoxImp {
|
|||||||
await _kr_ensureCommandClientInitialized();
|
await _kr_ensureCommandClientInitialized();
|
||||||
KRLogUtil.kr_i('✅ Command client 已就绪,执行节点切换', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ Command client 已就绪,执行节点切换', tag: 'SingBox');
|
||||||
|
|
||||||
// 🔧 关键修复:使用 await 等待节点选择完成
|
// 🔧 关键修复:使用正确的 group tag
|
||||||
KRLogUtil.kr_i('⏳ 调用 selectOutbound("select", "$tag")...', tag: 'SingBox');
|
// libcore 生成的selector组的tag是"proxy"而不是"select"
|
||||||
await _kr_selectOutboundWithRetry("select", tag, maxAttempts: 3, initialDelay: 50);
|
final selectorGroupTag = kr_activeGroups.any((g) => g.tag == 'select') ? 'select' : 'proxy';
|
||||||
|
KRLogUtil.kr_i('⏳ 调用 selectOutbound("$selectorGroupTag", "$tag")...', tag: 'SingBox');
|
||||||
|
await _kr_selectOutboundWithRetry(selectorGroupTag, tag, maxAttempts: 3, initialDelay: 50);
|
||||||
KRLogUtil.kr_i('✅ 节点切换API调用完成: $tag', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ 节点切换API调用完成: $tag', tag: 'SingBox');
|
||||||
|
|
||||||
// 🔧 新增:验证节点切换是否生效
|
// 🔧 新增:验证节点切换是否生效
|
||||||
@ -1607,7 +1832,6 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 配合文件地址
|
/// 配合文件地址
|
||||||
|
|
||||||
Directory get directory =>
|
Directory get directory =>
|
||||||
@ -1657,221 +1881,4 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_e('📚 错误详情: ${e.toString()}', tag: 'SingBox');
|
KRLogUtil.kr_e('📚 错误详情: ${e.toString()}', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> kr_debugTunConnectivity() async {
|
|
||||||
KRLogUtil.kr_i('🧪 [TUN Debug] 开始调试', tag: 'SingBoxTun');
|
|
||||||
final statusType = kr_status.value.runtimeType.toString();
|
|
||||||
KRLogUtil.kr_i('🔎 当前状态: $statusType', tag: 'SingBoxTun');
|
|
||||||
|
|
||||||
final selectedCountry = await KRSecureStorage().kr_readData(key: 'SELECTED_COUNTRY_TAG');
|
|
||||||
final selectedNode = await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
|
|
||||||
KRLogUtil.kr_i('🎯 选择: country=$selectedCountry, node=$selectedNode', tag: 'SingBox');
|
|
||||||
|
|
||||||
final activeTags = kr_activeGroups.map((g) => g.tag).join(', ');
|
|
||||||
KRLogUtil.kr_i('🧩 分组: all=${kr_allGroups.length}, active=${kr_activeGroups.length} [$activeTags]', tag: 'SingBoxTun');
|
|
||||||
|
|
||||||
// 打印每个 active 分组的节点信息
|
|
||||||
for (var group in kr_activeGroups) {
|
|
||||||
KRLogUtil.kr_i(
|
|
||||||
'📊 组[${group.tag}] 类型=${group.type} 节点数=${group.items.length}',
|
|
||||||
tag: 'SingBoxTun');
|
|
||||||
final selected = group.selected;
|
|
||||||
KRLogUtil.kr_i('🎯 当前选中节点: $selected', tag: 'SingBoxTun');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1️⃣ TCP 测试
|
|
||||||
try {
|
|
||||||
final sock =
|
|
||||||
await Socket.connect('1.1.1.1', 443, timeout: const Duration(seconds: 3));
|
|
||||||
sock.destroy();
|
|
||||||
KRLogUtil.kr_i('🌐 TCP 443连接成功 (1.1.1.1:443)', tag: 'SingBoxTun');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('🌐 TCP 443连接失败: $e', tag: 'SingBoxTun');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2️⃣ HTTPS 测试(Google Connectivity)
|
|
||||||
try {
|
|
||||||
final client = HttpClient()..connectionTimeout = const Duration(seconds: 3);
|
|
||||||
final req = await client
|
|
||||||
.getUrl(Uri.parse('https://connectivitycheck.gstatic.com/generate_204'));
|
|
||||||
req.headers.add('User-Agent', 'kr-debug');
|
|
||||||
final resp = await req.close();
|
|
||||||
KRLogUtil.kr_i('🔐 HTTPS 204 状态: ${resp.statusCode}', tag: 'SingBoxTun');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('🔐 HTTPS 204 错误: $e', tag: 'SingBoxTun');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3️⃣ 系统DNS解析
|
|
||||||
try {
|
|
||||||
final addrs = await InternetAddress.lookup('google.com');
|
|
||||||
KRLogUtil.kr_i(
|
|
||||||
'🧭 DNS解析成功: ${addrs.map((a) => a.address).join(", ")}',
|
|
||||||
tag: 'SingBoxTun');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('🧭 DNS解析错误: $e', tag: 'SingBoxTun');
|
|
||||||
}
|
|
||||||
// 3️⃣ 系统DNS解析
|
|
||||||
try {
|
|
||||||
final addrs = await InternetAddress.lookup('1.1.1.1');
|
|
||||||
KRLogUtil.kr_i(
|
|
||||||
'🧭 DNS解析成功1.1.1.1: ${addrs.map((a) => a.address).join(", ")}',
|
|
||||||
tag: 'SingBoxTun');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_e('🧭 DNS解析错误1.1.1.1: $e', tag: 'SingBoxTun');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4️⃣ TCP 53测试
|
|
||||||
try {
|
|
||||||
final sock =
|
|
||||||
await Socket.connect('8.8.8.8', 53, timeout: const Duration(seconds: 3));
|
|
||||||
sock.destroy();
|
|
||||||
KRLogUtil.kr_i('🛰️ TCP 53连接成功 (8.8.8.8:53)', tag: 'SingBoxTun');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_w('🛰️ TCP 53连接失败: $e', tag: 'SingBoxTun');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5️⃣ 检查出口 IP
|
|
||||||
try {
|
|
||||||
final client = HttpClient()..connectionTimeout = const Duration(seconds: 3);
|
|
||||||
final req = await client.getUrl(Uri.parse('https://api.ipify.org'));
|
|
||||||
final resp = await req.close();
|
|
||||||
final ip = await resp.transform(utf8.decoder).join();
|
|
||||||
KRLogUtil.kr_i('🌍 出口IP检测成功: $ip', tag: 'SingBoxTun');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_w('🌍 出口IP检测失败: $e', tag: 'SingBoxTun');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6️⃣ 网络类型(仅在移动端)
|
|
||||||
try {
|
|
||||||
final connectivity = await (Connectivity().checkConnectivity());
|
|
||||||
KRLogUtil.kr_i('📶 当前网络类型: $connectivity', tag: 'SingBoxTun');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_w('📶 网络类型检测失败: $e', tag: 'SingBoxTun');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7️⃣ HTTP延迟测试(快速验证)
|
|
||||||
try {
|
|
||||||
final sw = Stopwatch()..start();
|
|
||||||
final client = HttpClient()..connectionTimeout = const Duration(seconds: 3);
|
|
||||||
final req = await client
|
|
||||||
.getUrl(Uri.parse('https://www.google.com/generate_204'));
|
|
||||||
final resp = await req.close();
|
|
||||||
sw.stop();
|
|
||||||
KRLogUtil.kr_i('⏱️ HTTP延迟: ${sw.elapsedMilliseconds}ms (status=${resp.statusCode})',
|
|
||||||
tag: 'SingBoxTun');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_w('⏱️ HTTP延迟测试失败: $e', tag: 'SingBoxTun');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7️⃣ 检查出口国家信息(通过 geoip API)
|
|
||||||
try {
|
|
||||||
final client = HttpClient();
|
|
||||||
final req = await client.getUrl(Uri.parse('https://ipapi.co/json/'));
|
|
||||||
final resp = await req.close();
|
|
||||||
final data = await resp.transform(utf8.decoder).join();
|
|
||||||
KRLogUtil.kr_i('🌎 GeoIP: $data', tag: 'SingBoxTun');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_w('🌎 GeoIP检测失败: $e', tag: 'SingBoxTun');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8️⃣ 简单测速(仅调试)
|
|
||||||
final stopwatch = Stopwatch()..start();
|
|
||||||
try {
|
|
||||||
final client = HttpClient();
|
|
||||||
final req = await client.getUrl(Uri.parse('https://speed.cloudflare.com/__down?bytes=1000000'));
|
|
||||||
final resp = await req.close();
|
|
||||||
final totalBytes = await resp.fold<int>(0, (prev, e) => prev + e.length);
|
|
||||||
stopwatch.stop();
|
|
||||||
KRLogUtil.kr_i('⚡ 下载测试: ${totalBytes ~/ 1024} KB in ${stopwatch.elapsedMilliseconds}ms', tag: 'SingBoxTun');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_w('⚡ 下载测试失败: $e', tag: 'SingBoxTun');
|
|
||||||
}
|
|
||||||
|
|
||||||
KRLogUtil.kr_i('✅ [TUN Debug] 结束调试', tag: 'SingBoxTun');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据所选国家解析当前实际连接节点(适配 auto 选择)
|
|
||||||
// 策略:
|
|
||||||
// 1) 优先在 activeGroups 中查找 tag 与国家匹配的分组(完全匹配或包含关系)
|
|
||||||
// 2) 使用该分组的 selected 字段在 items 中找到具体节点
|
|
||||||
// 3) 若找不到,退化为在所有分组中查找其 items 是否包含与国家相关的节点,并返回其 selected 对应节点
|
|
||||||
SingboxOutboundGroupItem? _kr_resolveCurrentNodeForCountry(String? selectedCountry) {
|
|
||||||
if (selectedCountry == null || selectedCountry.isEmpty) return null;
|
|
||||||
final countryLower = selectedCountry.toLowerCase();
|
|
||||||
|
|
||||||
// 1) 直接通过分组 tag 匹配国家
|
|
||||||
for (final g in kr_activeGroups) {
|
|
||||||
final tagLower = g.tag.toLowerCase();
|
|
||||||
final tagMatch = tagLower == countryLower || tagLower.contains(countryLower);
|
|
||||||
if (tagMatch) {
|
|
||||||
final selTag = g.selected;
|
|
||||||
for (final item in g.items) {
|
|
||||||
if (item.tag == selTag) return item;
|
|
||||||
}
|
|
||||||
// 如果 selected 指向的不是本组的叶子,直接返回 null(调用方仍可查看组日志)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) 在所有分组中尝试通过 items 与国家关联性匹配
|
|
||||||
for (final g in kr_activeGroups) {
|
|
||||||
final maybeCountryRelated = g.items.any(
|
|
||||||
(i) => i.tag.toLowerCase().contains(countryLower));
|
|
||||||
if (maybeCountryRelated) {
|
|
||||||
final selTag = g.selected;
|
|
||||||
for (final item in g.items) {
|
|
||||||
if (item.tag == selTag) return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 启动自动切换调试订阅:实时打印 active-groups 的选中节点变化
|
|
||||||
void kr_startAutoSwitchDebug() {
|
|
||||||
// 这里不做重复订阅的严格检查,交由调用方控制多次调用
|
|
||||||
final sub = kr_singBox.watchActiveGroups().listen((groups) {
|
|
||||||
// 更新本地状态,供其他调试方法读取
|
|
||||||
kr_activeGroups.value = groups;
|
|
||||||
|
|
||||||
for (final g in groups) {
|
|
||||||
final selected = g.selected;
|
|
||||||
final selItem = g.items.firstWhere(
|
|
||||||
(i) => i.tag == selected,
|
|
||||||
orElse: () => SingboxOutboundGroupItem(
|
|
||||||
tag: '(未知)',
|
|
||||||
type: ProxyType.unknown,
|
|
||||||
urlTestDelay: -1,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
KRLogUtil.kr_i(
|
|
||||||
'🔄 组[${g.tag}] 选中 -> ${selItem.tag} (${selItem.type.label}), delay=${selItem.urlTestDelay}ms',
|
|
||||||
tag: 'SingBoxAuto',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, onError: (e) {
|
|
||||||
KRLogUtil.kr_e('❌ AutoSwitch 调试订阅错误: $e', tag: 'SingBoxAuto');
|
|
||||||
});
|
|
||||||
_kr_subscriptions.add(sub);
|
|
||||||
KRLogUtil.kr_i('✅ AutoSwitch 调试订阅已开启', tag: 'SingBox');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 通过 DoH 进行 DNS 解析探针(走代理),便于判断 DNS 失败是否触发切换
|
|
||||||
Future<void> kr_probeDoH(List<String> names) async {
|
|
||||||
final client = HttpClient()..connectionTimeout = const Duration(seconds: 5);
|
|
||||||
client.findProxy = (_) => kr_buildProxyRule();
|
|
||||||
for (final name in names) {
|
|
||||||
try {
|
|
||||||
final uri = Uri.parse('https://dns.google/resolve?name=$name&type=A');
|
|
||||||
final req = await client.getUrl(uri);
|
|
||||||
final resp = await req.close();
|
|
||||||
final body = await resp.transform(utf8.decoder).join();
|
|
||||||
KRLogUtil.kr_i('🧪 DoH 解析 $name -> 状态 ${resp.statusCode}, 响应 $body', tag: 'SingBoxDoH');
|
|
||||||
} catch (e) {
|
|
||||||
KRLogUtil.kr_w('🧪 DoH 解析 $name 失败: $e', tag: 'SingBoxDoH');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,8 @@ enum KRCountry {
|
|||||||
ru('俄罗斯'),
|
ru('俄罗斯'),
|
||||||
id('印度尼西亚'),
|
id('印度尼西亚'),
|
||||||
tr('土耳其'),
|
tr('土耳其'),
|
||||||
br('巴西');
|
br('巴西'),
|
||||||
|
other('全局代理'); // ✅ 新增:全局代理模式(所有流量走代理,不添加国家直连规则)
|
||||||
|
|
||||||
final String kr_name;
|
final String kr_name;
|
||||||
const KRCountry(this.kr_name);
|
const KRCountry(this.kr_name);
|
||||||
@ -30,7 +31,7 @@ enum KRCountry {
|
|||||||
static KRCountry? kr_fromCode(String code) {
|
static KRCountry? kr_fromCode(String code) {
|
||||||
try {
|
try {
|
||||||
return KRCountry.values.firstWhere(
|
return KRCountry.values.firstWhere(
|
||||||
(country) => country.kr_code == code.toLowerCase(),
|
(country) => country.kr_code == code.toLowerCase(),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
@ -53,7 +54,7 @@ class KRCountryUtil {
|
|||||||
static Future<void> kr_init() async {
|
static Future<void> kr_init() async {
|
||||||
try {
|
try {
|
||||||
final String? kr_savedCountry =
|
final String? kr_savedCountry =
|
||||||
await _kr_storage.kr_readData(key: _kr_countryKey);
|
await _kr_storage.kr_readData(key: _kr_countryKey);
|
||||||
if (kr_savedCountry != null) {
|
if (kr_savedCountry != null) {
|
||||||
final KRCountry? kr_country = KRCountry.kr_fromCode(kr_savedCountry);
|
final KRCountry? kr_country = KRCountry.kr_fromCode(kr_savedCountry);
|
||||||
if (kr_country != null) {
|
if (kr_country != null) {
|
||||||
@ -66,7 +67,7 @@ class KRCountryUtil {
|
|||||||
} else {
|
} else {
|
||||||
kr_currentCountry.value = KRCountry.cn;
|
kr_currentCountry.value = KRCountry.cn;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
KRLogUtil.kr_e('初始化国家设置失败: $err', tag: 'CountryUtil');
|
KRLogUtil.kr_e('初始化国家设置失败: $err', tag: 'CountryUtil');
|
||||||
kr_currentCountry.value = KRCountry.cn;
|
kr_currentCountry.value = KRCountry.cn;
|
||||||
@ -106,9 +107,9 @@ class KRCountryUtil {
|
|||||||
return kr_currentCountry.value.kr_code;
|
return kr_currentCountry.value.kr_code;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String kr_getCurrentCountryName() {
|
static String kr_getCurrentCountryName() {
|
||||||
return kr_getCountryName(kr_currentCountry.value);
|
return kr_getCountryName(kr_currentCountry.value);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取国家名称
|
/// 获取国家名称
|
||||||
@ -131,12 +132,14 @@ class KRCountryUtil {
|
|||||||
return AppTranslations.kr_country.tr;
|
return AppTranslations.kr_country.tr;
|
||||||
case KRCountry.br:
|
case KRCountry.br:
|
||||||
return AppTranslations.kr_country.br;
|
return AppTranslations.kr_country.br;
|
||||||
|
case KRCountry.other:
|
||||||
|
return '全局代理'; // ✅ 新增:全局代理
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取所有支持的国家列表
|
/// 获取所有支持的国家列表
|
||||||
static List<KRCountry> kr_getSupportedCountries() {
|
static List<KRCountry> kr_getSupportedCountries() {
|
||||||
if (AppConfig().kr_is_daytime == false) {
|
if (AppConfig().kr_is_daytime == false) {
|
||||||
return KRCountry.values.where((element) => element != KRCountry.cn).toList();
|
return KRCountry.values.where((element) => element != KRCountry.cn).toList();
|
||||||
}
|
}
|
||||||
return KRCountry.values;
|
return KRCountry.values;
|
||||||
@ -146,9 +149,9 @@ class KRCountryUtil {
|
|||||||
static List<Map<String, String>> kr_getCountryInfoList() {
|
static List<Map<String, String>> kr_getCountryInfoList() {
|
||||||
return KRCountry.values
|
return KRCountry.values
|
||||||
.map((country) => {
|
.map((country) => {
|
||||||
'code': country.kr_code,
|
'code': country.kr_code,
|
||||||
'name': country.kr_countryName,
|
'name': country.kr_countryName,
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
439
lib/app/utils/kr_windows_dns_util.dart
Normal file
439
lib/app/utils/kr_windows_dns_util.dart
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||||
|
|
||||||
|
/// Windows DNS 管理工具类
|
||||||
|
///
|
||||||
|
/// 用于在 Windows 平台上管理系统 DNS 设置
|
||||||
|
/// 主要功能:
|
||||||
|
/// 1. 备份原始 DNS 设置
|
||||||
|
/// 2. 恢复 DNS 设置
|
||||||
|
/// 3. 兜底设置为国内公共 DNS (223.5.5.5 和 114.114.114.114)
|
||||||
|
class KRWindowsDnsUtil {
|
||||||
|
/// 私有构造函数
|
||||||
|
KRWindowsDnsUtil._();
|
||||||
|
|
||||||
|
/// 单例实例
|
||||||
|
static final KRWindowsDnsUtil _instance = KRWindowsDnsUtil._();
|
||||||
|
|
||||||
|
/// 工厂构造函数
|
||||||
|
factory KRWindowsDnsUtil() => _instance;
|
||||||
|
|
||||||
|
/// 获取实例的静态方法
|
||||||
|
static KRWindowsDnsUtil get instance => _instance;
|
||||||
|
|
||||||
|
/// 原始 DNS 服务器地址(连接前备份)
|
||||||
|
List<String>? _originalDnsServers;
|
||||||
|
|
||||||
|
/// 主网络接口名称
|
||||||
|
String? _primaryInterfaceName;
|
||||||
|
|
||||||
|
/// 备份当前 DNS 设置
|
||||||
|
///
|
||||||
|
/// 在连接 VPN 之前调用,保存原始 DNS 配置
|
||||||
|
/// 返回:true-成功,false-失败
|
||||||
|
Future<bool> kr_backupDnsSettings() async {
|
||||||
|
if (!Platform.isWindows) {
|
||||||
|
KRLogUtil.kr_w('❌ 非 Windows 平台,跳过 DNS 备份', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
KRLogUtil.kr_i('📦 开始备份 Windows DNS 设置...', tag: 'WindowsDNS');
|
||||||
|
|
||||||
|
// 1. 获取主网络接口
|
||||||
|
final interfaceName = await _kr_getPrimaryNetworkInterface();
|
||||||
|
if (interfaceName == null) {
|
||||||
|
KRLogUtil.kr_e('❌ 无法获取主网络接口', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_primaryInterfaceName = interfaceName;
|
||||||
|
KRLogUtil.kr_i('🔍 主网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
|
||||||
|
|
||||||
|
// 2. 获取当前 DNS 服务器
|
||||||
|
final dnsServers = await _kr_getCurrentDnsServers(interfaceName);
|
||||||
|
if (dnsServers.isEmpty) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 当前 DNS 为空,可能是自动获取', tag: 'WindowsDNS');
|
||||||
|
_originalDnsServers = []; // 空列表表示 DHCP 自动获取
|
||||||
|
} else {
|
||||||
|
_originalDnsServers = dnsServers;
|
||||||
|
KRLogUtil.kr_i('✅ 已备份 DNS: ${dnsServers.join(", ")}', tag: 'WindowsDNS');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ 备份 DNS 设置失败: $e', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 恢复原始 DNS 设置
|
||||||
|
///
|
||||||
|
/// 在断开 VPN 后调用,恢复备份的 DNS 配置
|
||||||
|
/// 如果恢复失败,会自动调用兜底机制设置为公共DNS
|
||||||
|
/// 返回:true-成功,false-失败
|
||||||
|
Future<bool> kr_restoreDnsSettings() async {
|
||||||
|
if (!Platform.isWindows) {
|
||||||
|
KRLogUtil.kr_w('❌ 非 Windows 平台,跳过 DNS 恢复', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
KRLogUtil.kr_i('🔄 开始恢复 Windows DNS 设置...', tag: 'WindowsDNS');
|
||||||
|
|
||||||
|
// 1. 检查是否有备份
|
||||||
|
if (_primaryInterfaceName == null) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 没有备份的网络接口,尝试自动检测', tag: 'WindowsDNS');
|
||||||
|
_primaryInterfaceName = await _kr_getPrimaryNetworkInterface();
|
||||||
|
if (_primaryInterfaceName == null) {
|
||||||
|
KRLogUtil.kr_e('❌ 无法检测网络接口,执行兜底恢复', tag: 'WindowsDNS');
|
||||||
|
return await _kr_fallbackRestoreDns();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 恢复原始 DNS
|
||||||
|
if (_originalDnsServers == null) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 没有备份的 DNS,执行兜底恢复', tag: 'WindowsDNS');
|
||||||
|
return await _kr_fallbackRestoreDns();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_originalDnsServers!.isEmpty) {
|
||||||
|
// 原本是 DHCP 自动获取
|
||||||
|
KRLogUtil.kr_i('🔄 恢复为 DHCP 自动获取 DNS', tag: 'WindowsDNS');
|
||||||
|
final success = await _kr_setDnsToAuto(_primaryInterfaceName!);
|
||||||
|
if (!success) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 恢复 DHCP 失败,执行兜底恢复', tag: 'WindowsDNS');
|
||||||
|
return await _kr_fallbackRestoreDns();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 恢复指定的 DNS 服务器
|
||||||
|
KRLogUtil.kr_i('🔄 恢复原始 DNS: ${_originalDnsServers!.join(", ")}', tag: 'WindowsDNS');
|
||||||
|
final success = await _kr_setDnsServers(
|
||||||
|
_primaryInterfaceName!,
|
||||||
|
_originalDnsServers!,
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 恢复原始 DNS 失败,执行兜底恢复', tag: 'WindowsDNS');
|
||||||
|
return await _kr_fallbackRestoreDns();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 验证 DNS 是否恢复成功
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
final currentDns = await _kr_getCurrentDnsServers(_primaryInterfaceName!);
|
||||||
|
KRLogUtil.kr_i('✅ 当前 DNS: ${currentDns.join(", ")}', tag: 'WindowsDNS');
|
||||||
|
|
||||||
|
// 4. 额外验证:确认 DNS 不再指向 127.0.0.1(sing-box 的本地 DNS)
|
||||||
|
final hasLocalhost = currentDns.any((dns) => dns.startsWith('127.'));
|
||||||
|
if (hasLocalhost) {
|
||||||
|
KRLogUtil.kr_w('⚠️ DNS 仍包含 127.0.0.1,可能未完全恢复', tag: 'WindowsDNS');
|
||||||
|
KRLogUtil.kr_w('⚠️ 执行兜底恢复', tag: 'WindowsDNS');
|
||||||
|
return await _kr_fallbackRestoreDns();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ 恢复 DNS 设置失败: $e', tag: 'WindowsDNS');
|
||||||
|
KRLogUtil.kr_w('⚠️ 执行兜底恢复', tag: 'WindowsDNS');
|
||||||
|
return await _kr_fallbackRestoreDns();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 兜底恢复:强制设置为国内公共 DNS
|
||||||
|
///
|
||||||
|
/// 当正常恢复失败时,设置为安全的公共 DNS 服务器
|
||||||
|
/// - 主 DNS: 223.5.5.5 (阿里云)
|
||||||
|
/// - 备用 DNS: 114.114.114.114 (114DNS)
|
||||||
|
Future<bool> _kr_fallbackRestoreDns() async {
|
||||||
|
try {
|
||||||
|
KRLogUtil.kr_w('🆘 执行 DNS 兜底恢复机制', tag: 'WindowsDNS');
|
||||||
|
KRLogUtil.kr_i('🔧 设置为国内公共 DNS: 223.5.5.5, 114.114.114.114', tag: 'WindowsDNS');
|
||||||
|
|
||||||
|
// 1. 获取主网络接口(如果还没有)
|
||||||
|
if (_primaryInterfaceName == null) {
|
||||||
|
_primaryInterfaceName = await _kr_getPrimaryNetworkInterface();
|
||||||
|
if (_primaryInterfaceName == null) {
|
||||||
|
KRLogUtil.kr_e('❌ 无法检测网络接口,兜底恢复失败', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 设置为公共 DNS
|
||||||
|
final fallbackDns = ['223.5.5.5', '114.114.114.114'];
|
||||||
|
final success = await _kr_setDnsServers(_primaryInterfaceName!, fallbackDns);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
KRLogUtil.kr_i('✅ 兜底 DNS 设置成功', tag: 'WindowsDNS');
|
||||||
|
|
||||||
|
// 3. 验证设置
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
final currentDns = await _kr_getCurrentDnsServers(_primaryInterfaceName!);
|
||||||
|
KRLogUtil.kr_i('✅ 验证当前 DNS: ${currentDns.join(", ")}', tag: 'WindowsDNS');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
KRLogUtil.kr_e('❌ 兜底 DNS 设置失败', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ 兜底恢复失败: $e', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取主网络接口名称
|
||||||
|
///
|
||||||
|
/// 通过 netsh 命令查找处于"已连接"状态的主网络接口
|
||||||
|
/// 返回:接口名称,失败返回 null
|
||||||
|
Future<String?> _kr_getPrimaryNetworkInterface() async {
|
||||||
|
try {
|
||||||
|
// 使用 netsh 获取接口列表
|
||||||
|
final result = await Process.run('netsh', ['interface', 'show', 'interface']);
|
||||||
|
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
KRLogUtil.kr_e('❌ 获取网络接口失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final output = result.stdout.toString();
|
||||||
|
final lines = output.split('\n');
|
||||||
|
|
||||||
|
// 收集所有已连接的接口
|
||||||
|
final connectedInterfaces = <String>[];
|
||||||
|
|
||||||
|
// 查找所有"已连接"的接口
|
||||||
|
// 支持中文和英文 Windows 系统
|
||||||
|
for (var line in lines) {
|
||||||
|
// 中文: "已连接", 英文: "Connected", "Enabled"
|
||||||
|
if (line.contains('已连接') ||
|
||||||
|
line.contains('Connected') ||
|
||||||
|
line.toLowerCase().contains('enabled')) {
|
||||||
|
// 跳过表头行
|
||||||
|
if (line.contains('Admin') ||
|
||||||
|
line.contains('管理') ||
|
||||||
|
line.contains('State') ||
|
||||||
|
line.contains('状态')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析接口名称(最后一列)
|
||||||
|
final parts = line.trim().split(RegExp(r'\s{2,}'));
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
final interfaceName = parts.last.trim();
|
||||||
|
// 排除空接口名
|
||||||
|
if (interfaceName.isNotEmpty && interfaceName.length > 1) {
|
||||||
|
connectedInterfaces.add(interfaceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectedInterfaces.isEmpty) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 未找到已连接的网络接口', tag: 'WindowsDNS');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔧 优化:优先选择有线网络(以太网),然后才是 Wi-Fi
|
||||||
|
// 有线网络通常更稳定
|
||||||
|
String? selectedInterface;
|
||||||
|
for (var interface in connectedInterfaces) {
|
||||||
|
final lowerName = interface.toLowerCase();
|
||||||
|
// 优先选择以太网
|
||||||
|
if (lowerName.contains('ethernet') ||
|
||||||
|
lowerName.contains('以太网') ||
|
||||||
|
lowerName.contains('lan') ||
|
||||||
|
lowerName.contains('local')) {
|
||||||
|
selectedInterface = interface;
|
||||||
|
KRLogUtil.kr_i('🔍 选择有线网络接口: $interface', tag: 'WindowsDNS');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有有线网络,选择第一个(通常是 Wi-Fi)
|
||||||
|
selectedInterface ??= connectedInterfaces.first;
|
||||||
|
if (selectedInterface != connectedInterfaces.first) {
|
||||||
|
KRLogUtil.kr_d('🔍 选择网络接口: $selectedInterface', tag: 'WindowsDNS');
|
||||||
|
} else {
|
||||||
|
KRLogUtil.kr_i('🔍 选择网络接口: $selectedInterface', tag: 'WindowsDNS');
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedInterface;
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ 获取网络接口异常: $e', tag: 'WindowsDNS');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取指定接口的当前 DNS 服务器
|
||||||
|
///
|
||||||
|
/// 参数:
|
||||||
|
/// - interfaceName: 网络接口名称
|
||||||
|
///
|
||||||
|
/// 返回:DNS 服务器列表
|
||||||
|
Future<List<String>> _kr_getCurrentDnsServers(String interfaceName) async {
|
||||||
|
try {
|
||||||
|
final result = await Process.run('netsh', [
|
||||||
|
'interface',
|
||||||
|
'ipv4',
|
||||||
|
'show',
|
||||||
|
'dnsservers',
|
||||||
|
'name="$interfaceName"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
KRLogUtil.kr_e('❌ 获取 DNS 失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final output = result.stdout.toString();
|
||||||
|
final dnsServers = <String>[];
|
||||||
|
|
||||||
|
// 解析 DNS 服务器地址
|
||||||
|
final lines = output.split('\n');
|
||||||
|
for (var line in lines) {
|
||||||
|
// 查找 IP 地址格式的行
|
||||||
|
final ipMatch = RegExp(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b').firstMatch(line);
|
||||||
|
if (ipMatch != null) {
|
||||||
|
final ip = ipMatch.group(0)!;
|
||||||
|
// 排除本地回环地址
|
||||||
|
if (!ip.startsWith('127.')) {
|
||||||
|
dnsServers.add(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KRLogUtil.kr_d('🔍 当前 DNS: ${dnsServers.join(", ")}', tag: 'WindowsDNS');
|
||||||
|
return dnsServers;
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ 获取 DNS 异常: $e', tag: 'WindowsDNS');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置指定接口的 DNS 服务器
|
||||||
|
///
|
||||||
|
/// 参数:
|
||||||
|
/// - interfaceName: 网络接口名称
|
||||||
|
/// - dnsServers: DNS 服务器列表
|
||||||
|
///
|
||||||
|
/// 返回:true-成功,false-失败
|
||||||
|
Future<bool> _kr_setDnsServers(String interfaceName, List<String> dnsServers) async {
|
||||||
|
try {
|
||||||
|
if (dnsServers.isEmpty) {
|
||||||
|
KRLogUtil.kr_w('⚠️ DNS 列表为空,无法设置', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 设置主 DNS
|
||||||
|
KRLogUtil.kr_i('🔧 设置主 DNS: ${dnsServers[0]}', tag: 'WindowsDNS');
|
||||||
|
var result = await Process.run('netsh', [
|
||||||
|
'interface',
|
||||||
|
'ipv4',
|
||||||
|
'set',
|
||||||
|
'dnsservers',
|
||||||
|
'name="$interfaceName"',
|
||||||
|
'source=static',
|
||||||
|
'address=${dnsServers[0]}',
|
||||||
|
'validate=no',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
KRLogUtil.kr_e('❌ 设置主 DNS 失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 设置备用 DNS(如果有)
|
||||||
|
if (dnsServers.length > 1) {
|
||||||
|
for (int i = 1; i < dnsServers.length; i++) {
|
||||||
|
KRLogUtil.kr_i('🔧 设置备用 DNS ${i}: ${dnsServers[i]}', tag: 'WindowsDNS');
|
||||||
|
result = await Process.run('netsh', [
|
||||||
|
'interface',
|
||||||
|
'ipv4',
|
||||||
|
'add',
|
||||||
|
'dnsservers',
|
||||||
|
'name="$interfaceName"',
|
||||||
|
'address=${dnsServers[i]}',
|
||||||
|
'index=${i + 1}',
|
||||||
|
'validate=no',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 设置备用 DNS $i 失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||||
|
// 继续设置下一个,不中断
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 刷新 DNS 缓存
|
||||||
|
await _kr_flushDnsCache();
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ DNS 服务器设置完成', tag: 'WindowsDNS');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ 设置 DNS 异常: $e', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置 DNS 为自动获取(DHCP)
|
||||||
|
///
|
||||||
|
/// 参数:
|
||||||
|
/// - interfaceName: 网络接口名称
|
||||||
|
///
|
||||||
|
/// 返回:true-成功,false-失败
|
||||||
|
Future<bool> _kr_setDnsToAuto(String interfaceName) async {
|
||||||
|
try {
|
||||||
|
KRLogUtil.kr_i('🔧 设置 DNS 为自动获取 (DHCP)', tag: 'WindowsDNS');
|
||||||
|
|
||||||
|
final result = await Process.run('netsh', [
|
||||||
|
'interface',
|
||||||
|
'ipv4',
|
||||||
|
'set',
|
||||||
|
'dnsservers',
|
||||||
|
'name="$interfaceName"',
|
||||||
|
'source=dhcp',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
KRLogUtil.kr_e('❌ 设置 DHCP 失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新 DNS 缓存
|
||||||
|
await _kr_flushDnsCache();
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ DNS 已设置为自动获取', tag: 'WindowsDNS');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ 设置 DHCP 异常: $e', tag: 'WindowsDNS');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 刷新 DNS 缓存
|
||||||
|
///
|
||||||
|
/// 执行 ipconfig /flushdns 命令清空 DNS 解析缓存
|
||||||
|
Future<void> _kr_flushDnsCache() async {
|
||||||
|
try {
|
||||||
|
KRLogUtil.kr_i('🔄 刷新 DNS 缓存...', tag: 'WindowsDNS');
|
||||||
|
|
||||||
|
final result = await Process.run('ipconfig', ['/flushdns']);
|
||||||
|
|
||||||
|
if (result.exitCode == 0) {
|
||||||
|
KRLogUtil.kr_i('✅ DNS 缓存已刷新', tag: 'WindowsDNS');
|
||||||
|
} else {
|
||||||
|
KRLogUtil.kr_w('⚠️ 刷新 DNS 缓存失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 刷新 DNS 缓存异常: $e', tag: 'WindowsDNS');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清除备份数据
|
||||||
|
///
|
||||||
|
/// 在应用退出或不需要时调用
|
||||||
|
void kr_clearBackup() {
|
||||||
|
_originalDnsServers = null;
|
||||||
|
_primaryInterfaceName = null;
|
||||||
|
KRLogUtil.kr_d('🗑️ 已清除 DNS 备份数据', tag: 'WindowsDNS');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user