alroyso 2 weeks ago
parent
commit
feecc11216

+ 91 - 37
lib/app/clash/service/clash_service.dart

@@ -99,63 +99,117 @@ class ClashService extends GetxController {
 
   }
 
-  Future<bool> startClashCore() async {
-    final timeout = const Duration(seconds: 30);
-    final checkInterval = const Duration(milliseconds: 200);
-    var startTime = DateTime.now();
-
-    await controllers.config.readClashCoreApi();
-
+  // 启动普通模式的核心进程
+  Future<bool> startNormalCore() async {
     try {
-      controllers.global.updateMsg("启动内核---${controllers.config.config.value.selected}");
-      if (controllers.config.config.value.selected == controllers.config.config.value.selected) {
-        controllers.global.updateMsg("启动内核初始化");
-      } else {
-        controllers.global.updateMsg("启动内核");
-      }
+      // 先停止所有现有进程
+      // await controllers.global.stopAllCore();
+      
       coreStatus.value = RunningState.starting;
+      controllers.global.updateMsg("启动普通模式内核...");
 
-      int? exitCode;
       clashCoreProcess = await Process.start(
-          Files.assetsCCore.path,
-          ['-d', Paths.config.path, '-f', path.join(Paths.config.path, controllers.config.config.value.selected)],
-          mode: ProcessStartMode.inheritStdio
+        Files.assetsCCore.path,
+        ['-d', Paths.config.path, '-f', path.join(Paths.config.path, controllers.config.config.value.selected)],
+        mode: ProcessStartMode.inheritStdio
       );
-      clashCoreProcess!.exitCode.then((code) => exitCode = code);
-      if (exitCode != null && exitCode != 0) {
-        controllers.global.updateMsg("启动内核错误,请重启点电脑测试");
+
+      // 等待进程启动并检查API可用性
+      if (!await _waitForCoreReady()) {
         return false;
       }
 
-      controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
+      coreStatus.value = RunningState.running;
+      controllers.global.updateMsg("普通模式内核启动成功");
+      return true;
+    } catch (e) {
+      controllers.global.updateMsg("启动内核错误: $e");
+      coreStatus.value = RunningState.error;
+      return false;
+    }
+  }
 
-      while (DateTime.now().difference(startTime) < timeout) {
-        try {
-          controllers.global.updateMsg("等待内核启动..");
-          await controllers.core.fetchHello();
-          break;
-        } catch (_) {
-          await Future.delayed(checkInterval);
-        }
+  // 启动TUN模式的核心进程
+  Future<bool> startTunCore(String password) async {
+    Process? process;
+    try {
+      // 先停止所有现有进程
+      await controllers.global.stopAllCore();
+      
+      coreStatus.value = RunningState.starting;
+      controllers.global.updateMsg("启动TUN模式内核...");
+
+      final corePath = path.join(Paths.assetsBin.path, path.basename(Files.assetsCCore.path));
+      final configPath = path.join(Paths.config.path, controllers.config.config.value.selected);
+
+      process = await Process.start('bash', [
+        '-c',
+        'echo "$password" | sudo -S $corePath -d ${Paths.config.path} -f $configPath'
+      ], mode: ProcessStartMode.inheritStdio);
+
+      clashCoreProcess = process;
+
+      // 等待root进程启动
+      if (!await _waitForRootProcess()) {
+        throw Exception("TUN模式进程启动失败");
       }
 
-      if (DateTime.now().difference(startTime) >= timeout) {
-        coreStatus.value = RunningState.error;
-        controllers.global.updateMsg("内核启动超时,重新点击加速后尝试。");
-        return false;
+      // 等待API可用
+      if (!await _waitForCoreReady()) {
+        throw Exception("TUN模式API初始化失败");
       }
-      
-      await controllers.core.updateConfig();
+
       coreStatus.value = RunningState.running;
-      controllers.global.updateMsg("点击连接 ");
+      controllers.global.updateMsg("TUN模式内核启动成功");
       return true;
     } catch (e) {
-      controllers.global.updateMsg("启动内核错误");
+      process?.kill();
+      controllers.global.updateMsg("启动TUN模式失败: $e");
       coreStatus.value = RunningState.error;
       return false;
     }
   }
 
+  // 等待进程API就绪
+  Future<bool> _waitForCoreReady() async {
+    final timeout = const Duration(seconds: 30);
+    final checkInterval = const Duration(milliseconds: 200);
+    final startTime = DateTime.now();
+
+    controllers.core.setApi(
+      controllers.config.clashCoreApiAddress.value,
+      controllers.config.clashCoreApiSecret.value
+    );
+
+    while (DateTime.now().difference(startTime) < timeout) {
+      try {
+        controllers.global.updateMsg("等待内核启动..");
+        await controllers.core.fetchHello();
+        return true;
+      } catch (_) {
+        await Future.delayed(checkInterval);
+      }
+    }
+
+    return false;
+  }
+
+  // 等待root进程启动
+  Future<bool> _waitForRootProcess() async {
+    int retryCount = 0;
+    while (retryCount < 10) {
+      final checkResult = await Process.run('bash', [
+        '-c',
+        'ps -ef | grep core-darwin | grep root | grep -v grep'
+      ]);
+      if (checkResult.exitCode == 0 && checkResult.stdout.toString().isNotEmpty) {
+        return true;
+      }
+      await Future.delayed(const Duration(milliseconds: 500));
+      retryCount++;
+    }
+    return false;
+  }
 
   Future<void> stopClashCore() async {
     coreStatus.value = RunningState.stopping;

+ 30 - 2
lib/app/controller/GlobalController.dart

@@ -113,12 +113,12 @@ class GlobalController extends GetxController {
         controllers.config.config.value.selected = Files.makeInitProxyConfig.path;
         
         if (Platform.isWindows) {
-          await controllers.service.startClashCore();
+          await controllers.service.startNormalCore();
         } else if (Platform.isMacOS) {
           if (controllers.cc_service.serviceIsRuning) {
             //await controllers.cc_service.fetchStart();
           } else {
-            await controllers.service.startClashCore();
+            await controllers.service.startNormalCore();
           }
         }
         
@@ -467,4 +467,32 @@ class GlobalController extends GetxController {
     controllers.window.dispose();
     super.dispose();
   }
+
+  Future<void> updateTunMode(bool enabled) async {
+    if (systemProxySwitchIng.value || tunProxySwitchIng.value) return;
+    
+    try {
+      tunProxySwitchIng.value = true;
+      
+      // 如果是macOS且启用TUN模式,先检查密码
+      if (Platform.isMacOS && enabled) {
+        final password = await getKeychainPassword();
+        if (password == null) {
+          throw Exception("未保存管理员密码,请先保存密码");
+        }
+      }
+      
+      // 更新路由模式
+      final newMode = enabled ? "tun" : "sys";
+      routeModesSelect.value = newMode;
+      
+    } catch (e) {
+      // 发生错误时恢复原来的模式
+      routeModesSelect.value = enabled ? "sys" : "tun";
+      handleApiError(e);
+      throw e;
+    } finally {
+      tunProxySwitchIng.value = false;
+    }
+  }
 }

+ 51 - 0
lib/app/controller/dialog.dart

@@ -8,6 +8,48 @@ class DialogController extends GetxController {
     isDialogVisible.value = true;
   }
 
+  Future<String?> showInputDialog({
+    required String title,
+    required String content,
+    String cancelText = "取消",
+    String enterText = "确定",
+    bool isPassword = false,
+  }) {
+    final controller = TextEditingController();
+    
+    return Get.dialog<String>(
+      AlertDialog(
+        title: Text(title),
+        content: Column(
+          mainAxisSize: MainAxisSize.min,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Text(content),
+            const SizedBox(height: 10),
+            TextField(
+              controller: controller,
+              obscureText: isPassword,
+              decoration: InputDecoration(
+                border: const OutlineInputBorder(),
+                hintText: isPassword ? "请输入密码" : "请输入",
+              ),
+            ),
+          ],
+        ),
+        actions: [
+          TextButton(
+            onPressed: () => Get.back(),
+            child: Text(cancelText),
+          ),
+          TextButton(
+            onPressed: () => Get.back(result: controller.text),
+            child: Text(enterText),
+          ),
+        ],
+      ),
+    );
+  }
+
   Future<bool?> showNormalDialog({
     required String title,
     String? content,
@@ -35,4 +77,13 @@ class DialogController extends GetxController {
       ),
     );
   }
+
+  void showToast(String message) {
+    Get.showSnackbar(
+      GetSnackBar(
+        message: message,
+        duration: const Duration(seconds: 2),
+      ),
+    );
+  }
 }

+ 1 - 1
lib/app/controller/service.dart

@@ -203,7 +203,7 @@ class ServiceController extends GetxController {
       await Future.delayed(Duration(seconds: 2));
       await controllers.service.makeInitConfig();
       await Future.delayed(Duration(seconds: 5));
-      await controllers.service.startClashCore();
+      await controllers.service.startNormalCore();
 
       serviceStatus.value = RunningState.stoped;
 

+ 9 - 9
lib/app/modules/home/controllers/home_controller.dart

@@ -142,15 +142,15 @@ class HomeController extends GetxController {
     } else {
 
 
-    if(Platform.isMacOS){
-      if(controllers.global.routeModesSelect.value  == "tun"){
-        //网卡模式
-        if(!controllers.cc_service.serviceIsRuning){
-          controllers.global.errorMsg.value = "通过右上角安装服务来获取管理员权限";
-          return;
-        }
-      }
-    }
+    // if(Platform.isMacOS){
+    //   if(controllers.global.routeModesSelect.value  == "tun"){
+    //     //网卡模式
+    //     if(!controllers.cc_service.serviceIsRuning){
+    //       controllers.global.errorMsg.value = "通过右上角安装服务来获取管理员权限";
+    //       return;
+    //     }
+    //   }
+    // }
 
 
       updateStatus(ConnectionStatus.connecting);

+ 60 - 7
lib/app/modules/home/views/home_view.dart

@@ -182,8 +182,10 @@ class HomeView extends GetView<HomeController> {
                   ),
                   const SizedBox(height: 20,),
                   Center(
-                    child: Row(
-                      mainAxisAlignment: MainAxisAlignment.center,
+                    child: Wrap(
+                      spacing: 10,
+                      runSpacing: 10,
+                      alignment: WrapAlignment.center,
                       children: [
                         ButtonSelect(
                           labels: ['setting_mode_rules'.tr,'setting_mode_global'.tr,],
@@ -192,14 +194,65 @@ class HomeView extends GetView<HomeController> {
                             controllers.global.updateMode(controllers.global.modes[idx])
                           },
                         ),
+                        Obx(() => ButtonSelect(
+                          labels: ['TUN关闭', 'TUN开启'],
+                          value: controllers.global.routeModesSelect.value == 'tun' ? 1 : 0,
+                          onSelect: (idx) async {
+                            final bool tunEnabled = idx == 1;
+                            try {
+                              if (tunEnabled && Platform.isMacOS) {
+                                final bool? result = await controllers.dialog.showNormalDialog(
+                                  title: "提示",
+                                  content: "开启TUN模式需要管理员权限,是否继续?",
+                                  cancelText: "取消",
+                                  enterText: "确定",
+                                );
+                                if (result != true) return;
+                              }
+                              await controllers.global.updateTunMode(tunEnabled);
+                            } catch (e) {
+                              // 操作失败时不改变状态
+                              return;
+                            }
+                          },
+                        )),
+                        if (Platform.isMacOS)
+                          SizedBox(
+                            height: 32,
+                            child: ElevatedButton(
+                              onPressed: () async {
+                                final result = await controllers.dialog.showInputDialog(
+                                  title: "输入管理员密码",
+                                  content: "请输入系统管理员密码,将安全保存用于TUN模式切换",
+                                  isPassword: true,
+                                );
+                                if (result != null && result.isNotEmpty) {
+                                  try {
+                                    await saveKeychainPassword(result);
+                                    controllers.dialog.showToast("密码已保存");
+                                  } catch (e) {
+                                    controllers.dialog.showToast("密码保存失败:$e");
+                                  }
+                                }
+                              },
+                              child: const Text("保存管理员密码"),
+                            ),
+                          ),
                       ],
                     ),
                   ),
                   const SizedBox(height: 20,),
-                  Text("版本号:${controllers.global.sysVersion.value}"),
-                  const SizedBox(height: 2,),
-                  if (controllers.global.sysInfo.value.isNotEmpty) 
-                    Text("当前:${controllers.global.sysInfo.value}"),
+                  Container(
+                    padding: const EdgeInsets.symmetric(vertical: 10),
+                    child: Column(
+                      children: [
+                        Text("版本号:${controllers.global.sysVersion.value}"),
+                        const SizedBox(height: 2),
+                        if (controllers.global.sysInfo.value.isNotEmpty) 
+                          Text("当前:${controllers.global.sysInfo.value}"),
+                      ],
+                    ),
+                  ),
                 ],
               ),
             );
@@ -306,6 +359,6 @@ class UserStatusWidget extends StatelessWidget {
                               }
                               // await controller.connectionService.checkServiceInstallation();
                             },
-                            child: Text(controllers.cc_service.installStatus.value ? '卸载���务' : '安装服务'),
+                            child: Text(controllers.cc_service.installStatus.value ? '卸载务' : '安装服务'),
                           ),
 */

+ 48 - 38
lib/app/service/connection_service.dart

@@ -8,12 +8,14 @@ import 'package:naiyouwl/app/clash/service/clash_service.dart';
 import 'package:naiyouwl/app/controller/service.dart';
 import 'package:naiyouwl/app/utils/shell.dart';
 import 'package:wl_base_help/wl_base_help.dart';
-
+import 'package:path/path.dart' as path;
+import '../const/const.dart';
 import '../controller/GlobalController.dart';
 import '../modules/home/controllers/home_controller.dart';
 import '../utils/WindowsProxyManager.dart';
 import '../utils/utils.dart';
-
+import 'dart:convert';
+import 'dart:async';
 class ConnectionService {
   final GlobalController globalController;
   final Function(ConnectionStatus) updateStatus;
@@ -22,17 +24,45 @@ class ConnectionService {
 
   ConnectionService(this.globalController, this.updateStatus);
   Future<void> startConnectionMac() async {
-    updateStatus(ConnectionStatus.connecting);
-    globalController.updateMsg("正在启动连接...");
-    await globalController.updateNode();
-    await controllers.cc_service.reloadClashCore();
-    await globalController.swift(globalController.selectedNode.value?.name ?? "");
-    // 等待连接建立
-    await Future.delayed(Duration(seconds: 2));
-    updateStatus(ConnectionStatus.connected);
-    globalController.connectStatus.value = true;
-    globalController.updateMsg("连接成功");
-    await globalController.openProxy();
+    try {
+      updateStatus(ConnectionStatus.connecting);
+      globalController.updateMsg("正在启动连接...");
+      
+      // 更新节点
+      await globalController.updateNode();
+      
+      bool success;
+      // 检查是否是TUN模式
+      if (globalController.routeModesSelect.value == "tun") {
+        final password = await getKeychainPassword();
+        if (password == null) {
+          throw Exception("未保存管理员密码,请先保存密码");
+        }
+        
+        await globalController.makeProxy();
+        success = await clashService.startTunCore(password);
+        if (!success) {
+          throw Exception("内核启动失败");
+        }
+      } else {
+         await controllers.cc_service.reloadClashCore();
+      }
+      
+
+      
+      await globalController.swift(globalController.selectedNode.value?.name ?? "");
+      await Future.delayed(const Duration(seconds: 2));
+      
+      updateStatus(ConnectionStatus.connected);
+      globalController.connectStatus.value = true;
+      globalController.updateMsg("连接成功");
+      await globalController.openProxy();
+      
+    } catch (e) {
+      updateStatus(ConnectionStatus.disconnected);
+      globalController.handleApiError("连接失败: $e");
+      throw e;
+    }
   }
 
   Future<void> startConnectionWin() async {
@@ -173,40 +203,20 @@ class ConnectionService {
     try {
       updateStatus(ConnectionStatus.disconnected);
       globalController.updateMsg("正在断开连接...");
-
+      
       if(Platform.isMacOS){
         if (serviceController.serviceIsRuning) {
-          //await serviceController.stopClash();
           await serviceController.fetchSetProxyStop();
-
-        } else {
-          if (controllers.service.clashServiceIsRuning){
-            await controllers.service.stopClash();
-            await globalController.closeProxy();
-            globalController.updateMsg("内核已重新加载");
-          } else {
-            globalController.updateMsg("内核启动失败");
-          }
-
-        }
-      } else{
-        if (controllers.service.clashServiceIsRuning){
-          await controllers.service.stopClash();
-          final proxyManager = WlBaseHelp();
-          await proxyManager.stopProxy();
-          // await globalController.closeProxy();
-          globalController.updateMsg("内核已重新加载");
         } else {
-          globalController.updateMsg("内核启动失败");
+          await controllers.cc_service.stopClash();
+          await globalController.closeProxy();
         }
-
       }
-
-
+      
       globalController.connectStatus.value = false;
       updateStatus(ConnectionStatus.disconnected);
       globalController.updateMsg("已断开连接");
-      controllers.global.connectStatus.value = false;
+      
     } catch (e) {
       globalController.handleApiError("断开连接失败: $e");
     }

+ 47 - 0
lib/app/utils/shell.dart

@@ -76,6 +76,53 @@ Future<ProcessResult> runCCoreAsAdmin(String executable, List<String> arguments)
     [executable, ...arguments],
   );
 }
+
+
+Future<String?> getKeychainPassword() async {
+  final result = await Process.run('security', [
+    'find-generic-password',
+    '-a', 'naiyout',
+    '-s', 'sudo_password',
+    '-w'
+  ]);
+  return result.exitCode == 0 ? result.stdout.trim() : null;
+}
+
+Future<bool> hasSavedPassword() async {
+  final password = await getKeychainPassword();
+  return password != null;
+}
+
+Future<bool> testSudoPassword(String password) async {
+  final result = await Process.run('bash', [
+    '-c',
+    'echo "$password" | sudo -S echo "test"'
+  ]);
+  return result.exitCode == 0;
+}
+
+Future<void> saveKeychainPassword(String password) async {
+  // 先测试密码是否正确
+  if (!await testSudoPassword(password)) {
+    throw Exception("密码不正确");
+  }
+  
+  // 删除旧密码(如果存在)
+  await Process.run('security', [
+    'delete-generic-password',
+    '-a', 'naiyout',
+    '-s', 'sudo_password'
+  ]);
+  
+  // 保存新密码
+  await Process.run('security', [
+    'add-generic-password',
+    '-a', 'naiyout',
+    '-s', 'sudo_password',
+    '-w', password
+  ]);
+}
+
 Future<ProcessResult> runAsAdmin(String executable, List<String> arguments) async {
   String executablePath = shellArgument(executable).replaceAll(' ', r'\\ ');
   //executablePath = executablePath.substring(1, executablePath.length - 1);