feat: 1.新增在 VPN 配置中添加了一条规则,2.修复全局代理无法使用
This commit is contained in:
@@ -16,16 +16,14 @@ import 'package:path/path.dart' as p;
|
||||
import '../../../core/model/directories.dart';
|
||||
import '../../../singbox/model/singbox_config_option.dart';
|
||||
import '../../../singbox/model/singbox_outbound.dart';
|
||||
import '../../../singbox/model/singbox_proxy_type.dart';
|
||||
import '../../../singbox/model/singbox_stats.dart';
|
||||
import '../../../singbox/model/singbox_status.dart';
|
||||
import '../../utils/kr_country_util.dart';
|
||||
import '../../utils/kr_log_util.dart';
|
||||
import '../../utils/kr_secure_storage.dart';
|
||||
import '../../utils/kr_windows_dns_util.dart';
|
||||
import '../../common/app_run_data.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
|
||||
|
||||
enum KRConnectionType {
|
||||
global,
|
||||
@@ -427,6 +425,12 @@ class KRSingBoxImp {
|
||||
);
|
||||
|
||||
KRLogUtil.kr_i('✅ SingBox 初始化完成');
|
||||
|
||||
// 🔧 提取 geosite 文件到 workingDir
|
||||
KRLogUtil.kr_i('📦 开始提取 geosite 文件...', tag: 'SingBox');
|
||||
await _kr_extractGeositeFiles();
|
||||
KRLogUtil.kr_i('✅ geosite 文件提取完成', tag: 'SingBox');
|
||||
|
||||
_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() {
|
||||
// 不使用缓存,每次都重新生成配置
|
||||
// if (kr_configOption.isNotEmpty) {
|
||||
// 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 = {
|
||||
"region": "other", // 参考 hiddify-app: 默认使用 "other" 跳过规则集下载
|
||||
"region": effectiveRegion, // 🔧 修复:根据出站模式动态设置 region
|
||||
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
|
||||
"use-xray-core-when-possible": false,
|
||||
"execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写
|
||||
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
|
||||
"resolve-destination": false,
|
||||
"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",
|
||||
"direct-dns-address": "local" ,
|
||||
"direct-dns-address": "local", // 使用系统 DNS,确保中转服务器域名能被解析
|
||||
"direct-dns-domain-strategy": "prefer_ipv4",
|
||||
"mixed-port": kr_port,
|
||||
"tproxy-port": kr_port,
|
||||
@@ -511,13 +571,21 @@ class KRSingBoxImp {
|
||||
"enable-tun": Platform.isIOS || Platform.isAndroid,
|
||||
"enable-tun-service": false,
|
||||
"set-system-proxy":
|
||||
Platform.isWindows || Platform.isLinux || Platform.isMacOS,
|
||||
Platform.isWindows || Platform.isLinux || Platform.isMacOS,
|
||||
"bypass-lan": false,
|
||||
"allow-connection-from-lan": false,
|
||||
"enable-fake-dns": false,
|
||||
"enable-dns-routing": 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": {
|
||||
"enable": false,
|
||||
"padding": false,
|
||||
@@ -562,6 +630,17 @@ class KRSingBoxImp {
|
||||
}
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1025,25 +1104,10 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i('💾 开始保存配置文件...', 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++) {
|
||||
final outbound = realNodes[i];
|
||||
KRLogUtil.kr_i('📋 真实节点[$i] 配置:', tag: 'SingBox');
|
||||
for (int i = 0; i < outbounds.length; i++) {
|
||||
final outbound = outbounds[i];
|
||||
KRLogUtil.kr_i('📋 节点[$i] 配置:', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - server: ${outbound['server']}', tag: 'SingBox');
|
||||
@@ -1051,26 +1115,20 @@ class KRSingBoxImp {
|
||||
if (outbound['method'] != null) {
|
||||
KRLogUtil.kr_i(' - method: ${outbound['method']}', tag: 'SingBox');
|
||||
}
|
||||
if (outbound['interval'] != null) {
|
||||
KRLogUtil.kr_i(' - interval: ${outbound['interval']}', tag: 'SingBox');
|
||||
}
|
||||
if (outbound['password'] != null) {
|
||||
KRLogUtil.kr_i(' - password: ${outbound['password']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
||||
}
|
||||
if (outbound['uuid'] != null) {
|
||||
KRLogUtil.kr_i(' - uuid: ${outbound['uuid']?.toString().substring(0, 8)}...', 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');
|
||||
}
|
||||
KRLogUtil.kr_i(' - 完整配置: ${jsonEncode(outbound)}', tag: 'SingBox');
|
||||
}
|
||||
|
||||
// ⚠️ 临时过滤 Hysteria2 节点以避免 libcore 崩溃
|
||||
final filteredRealNodes = realNodes.where((outbound) {
|
||||
kr_outbounds = outbounds.where((outbound) {
|
||||
final type = outbound['type'];
|
||||
if (type == 'hysteria2' || type == 'hysteria') {
|
||||
KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox');
|
||||
@@ -1079,30 +1137,11 @@ class KRSingBoxImp {
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
// 过滤自动选择节点中的 Hysteria2
|
||||
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));
|
||||
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
|
||||
|
||||
// 🔧 修复:生成完整的 SingBox 配置
|
||||
// 现在保存完整配置,包含所有必需字段,并且 outbounds 顺序为:selector→自动组→真实节点→direct/block/dns-out
|
||||
// 之前只保存 {"outbounds": [...]}, 导致 libcore 无法正确处理
|
||||
// 现在保存完整配置,包含所有必需字段
|
||||
final Map<String, dynamic> fullConfig = {
|
||||
"log": {
|
||||
"level": "debug",
|
||||
@@ -1112,8 +1151,7 @@ class KRSingBoxImp {
|
||||
"servers": [
|
||||
{
|
||||
"tag": "dns-remote",
|
||||
"address": "https://dns.google/dns-query",
|
||||
"detour": "direct",
|
||||
"address": "https://1.1.1.1/dns-query",
|
||||
"address_resolver": "dns-direct"
|
||||
},
|
||||
{
|
||||
@@ -1122,7 +1160,7 @@ class KRSingBoxImp {
|
||||
"detour": "direct"
|
||||
}
|
||||
],
|
||||
"rules": [],
|
||||
"rules": _kr_buildDnsRules(), // ✅ 使用动态构建的 DNS 规则
|
||||
"final": "dns-remote",
|
||||
"strategy": "prefer_ipv4"
|
||||
},
|
||||
@@ -1139,17 +1177,14 @@ class KRSingBoxImp {
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
// 🔧 修复:添加 selector 组,包含真实和自动节点标签
|
||||
// 🔧 修复:添加 selector 组,让用户可以手动选择节点
|
||||
{
|
||||
"type": "selector",
|
||||
"tag": "proxy",
|
||||
"outbounds": allAvailableTags,
|
||||
"default": allAvailableTags.isNotEmpty ? allAvailableTags[0] : "direct",
|
||||
"outbounds": kr_outbounds.map((o) => o['tag'] as String).toList(),
|
||||
"default": kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct",
|
||||
},
|
||||
// 🔧 修复:自动选择组排在真实节点前面
|
||||
...filteredAutoNodes,
|
||||
// 🔧 修复:真实节点排在后面
|
||||
...filteredRealNodes,
|
||||
...kr_outbounds,
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
@@ -1164,13 +1199,8 @@ class KRSingBoxImp {
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{ "type": "dns", "outbound": "dns-out" }, // sing-box 内部解析交给 dns-out
|
||||
{ "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 域名直连,避免误走代理
|
||||
],
|
||||
"rules": _kr_buildRouteRules(), // ✅ 使用动态构建的路由规则
|
||||
"rule_set": _kr_buildRuleSets(), // ✅ 使用动态构建的规则集
|
||||
"final": "proxy", // 🔧 修复:使用 selector 组作为默认出站
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
@@ -1181,8 +1211,7 @@ class KRSingBoxImp {
|
||||
final mapStr = jsonEncode(fullConfig);
|
||||
|
||||
KRLogUtil.kr_i('📄 完整配置文件长度: ${mapStr.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 Outbounds 总数: ${filteredAutoNodes.length + filteredRealNodes.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 Selector 可用标签数: ${allAvailableTags.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 Outbounds 数量: ${kr_outbounds.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}', tag: 'SingBox');
|
||||
|
||||
await file.writeAsString(mapStr);
|
||||
@@ -1200,6 +1229,128 @@ class KRSingBoxImp {
|
||||
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 {
|
||||
// 不再手动设置状态,libcore 会通过 status stream 自动发送状态更新
|
||||
try {
|
||||
@@ -1211,9 +1362,33 @@ class KRSingBoxImp {
|
||||
|
||||
// ⚠️ 强制编译标记 - v2.0-lazy-load
|
||||
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('📝 配置名称: $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 冲突
|
||||
// 注意:如果没有旧实例,libcore 会报错 "command.sock: no such file",这是正常的
|
||||
try {
|
||||
@@ -1367,12 +1542,12 @@ class KRSingBoxImp {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
// 添加超时保护,防止 stop() 调用阻塞
|
||||
// 缩短超时时间,避免用户等待过久
|
||||
// 🔧 延长超时时间到 10 秒,给 Windows DNS 清理足够时间
|
||||
try {
|
||||
await kr_singBox.stop().run().timeout(
|
||||
const Duration(seconds: 3),
|
||||
const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒
|
||||
onTimeout: () {
|
||||
KRLogUtil.kr_w('⚠️ 停止操作超时(3秒),强制继续', tag: 'SingBox');
|
||||
KRLogUtil.kr_w('⚠️ 停止操作超时(10秒),强制继续', tag: 'SingBox');
|
||||
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) {
|
||||
@@ -1403,6 +1600,17 @@ class KRSingBoxImp {
|
||||
} catch (e, stackTrace) {
|
||||
KRLogUtil.kr_e('停止服务时出错: $e');
|
||||
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 的状态管理
|
||||
rethrow;
|
||||
}
|
||||
@@ -1426,10 +1634,25 @@ class KRSingBoxImp {
|
||||
}
|
||||
|
||||
Future<void> kr_restart() async {
|
||||
KRLogUtil.kr_i("restart");
|
||||
kr_singBox.restart(_cutPath, kr_configName, false).mapLeft((err) {
|
||||
KRLogUtil.kr_e('重启失败: $err');
|
||||
}).run();
|
||||
KRLogUtil.kr_i('🔄 重启 SingBox...', tag: 'SingBox');
|
||||
|
||||
// 🔧 修复:使用 stop + start 而不是 restart
|
||||
// 这样可以触发 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();
|
||||
KRLogUtil.kr_i('✅ Command client 已就绪,执行节点切换', tag: 'SingBox');
|
||||
|
||||
// 🔧 关键修复:使用 await 等待节点选择完成
|
||||
KRLogUtil.kr_i('⏳ 调用 selectOutbound("select", "$tag")...', tag: 'SingBox');
|
||||
await _kr_selectOutboundWithRetry("select", tag, maxAttempts: 3, initialDelay: 50);
|
||||
// 🔧 关键修复:使用正确的 group tag
|
||||
// libcore 生成的selector组的tag是"proxy"而不是"select"
|
||||
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');
|
||||
|
||||
// 🔧 新增:验证节点切换是否生效
|
||||
@@ -1607,7 +1832,6 @@ class KRSingBoxImp {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 配合文件地址
|
||||
|
||||
Directory get directory =>
|
||||
@@ -1657,221 +1881,4 @@ class KRSingBoxImp {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user