import 'dart:async'; import 'dart:convert'; import 'dart:ffi' as ffi; import 'dart:io'; import 'dart:isolate'; import 'package:dio/dio.dart'; import 'package:naiyouwl/main.dart'; import 'package:dart_json_mapper/dart_json_mapper.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart'; import 'package:path/path.dart' as p; import 'package:get/get.dart'; import 'package:kommon/request/request.dart'; import 'package:kommon/tool/sp_util.dart'; import 'package:naiyouwl/clash_generated_bindings.dart'; import 'package:path_provider/path_provider.dart'; import 'package:proxy_manager/proxy_manager.dart'; import 'package:tray_manager/tray_manager.dart'; import '../bean/clash_config_entity.dart'; import '../data/model/NodeMode.dart'; late NativeLibrary clashFFI; //android 或者ios //const mobileChannel = MethodChannel("xxxPlugin"); class ClashService extends GetxService with TrayListener { // 需要一起改端口 static const clashBaseUrl = "http://127.0.0.1:$clashExtPort"; static const clashExtPort = 22346; // 运行时 late Directory _clashDirectory; RandomAccessFile? _clashLock; // 流量 final uploadRate = 0.0.obs; final downRate = 0.0.obs; final yamlConfigs = RxSet(); final currentYaml = 'config.yaml'.obs; final proxyStatus = RxMap(); final proxyYamlCurrent = "".obs; final proxyYaml = 'proxy.yaml'.obs; // action static const ACTION_SET_SYSTEM_PROXY = "assr"; static const ACTION_UNSET_SYSTEM_PROXY = "ausr"; static const MAX_ENTRIES = 5; // default port static var initializedHttpPort = 0; static var initializedSockPort = 0; static var initializedMixedPort = 0; // config Rx configEntity = Rx(null); // log Stream? logStream; RxMap proxies = RxMap(); RxBool isSystemProxyObs = RxBool(false); ClashService() { // load lib var fullPath = ""; if (Platform.isWindows) { fullPath = "libclash.dll"; } else if (Platform.isMacOS) { fullPath = "libclash.dylib"; } else { fullPath = "libclash.so"; } final lib = ffi.DynamicLibrary.open(fullPath); clashFFI = NativeLibrary(lib); clashFFI.init_native_api_bridge(ffi.NativeApi.initializeApiDLData); } Future init() async { _clashDirectory = await getApplicationSupportDirectory(); final _ = SpUtil.getData('yaml', defValue: currentYaml.value); initializedHttpPort = SpUtil.getData('http-port', defValue: 7899); initializedSockPort = SpUtil.getData('socks-port', defValue: 7877); initializedMixedPort = SpUtil.getData('mixed-port', defValue: 7811); currentYaml.value = _; Request.setBaseUrl(clashBaseUrl); final clashConfigPath = p.join(_clashDirectory.path, "clash"); _clashDirectory = Directory(clashConfigPath); if (kDebugMode) { print("fclash work directory: ${_clashDirectory.path}"); } final clashConf = p.join(_clashDirectory.path, currentYaml.value); final countryMMdb = p.join(_clashDirectory.path, 'Country.mmdb'); if (!await _clashDirectory.exists()) { await _clashDirectory.create(recursive: true); } // copy executable to directory final mmdb = await rootBundle.load('assets/tp/clash/Country.mmdb'); // write to clash dir final mmdbF = File(countryMMdb); if (!mmdbF.existsSync()) { await mmdbF.writeAsBytes(mmdb.buffer.asInt8List()); } final countryGeoIP= p.join(_clashDirectory.path, 'geoip.dat'); final geoip = await rootBundle.load('assets/tp/clash/geoip.dat'); // write to clash dir final geoipF = File(countryGeoIP); if (!geoipF.existsSync()) { await geoipF.writeAsBytes(geoip.buffer.asInt8List()); } final countryGeoSite= p.join(_clashDirectory.path, 'geosite.dat'); final geoSite = await rootBundle.load('assets/tp/clash/geoip.dat'); // write to clash dir final geoSiteF = File(countryGeoSite); if (!geoSiteF.existsSync()) { await geoSiteF.writeAsBytes(geoSite.buffer.asInt8List()); } final config = await rootBundle.load('assets/tp/clash/config.yaml'); // write to clash dir final configF = File(clashConf); if (!configF.existsSync()) { await configF.writeAsBytes(config.buffer.asInt8List()); } // create or detect lock file await _acquireLock(_clashDirectory); // ffi clashFFI.set_home_dir(_clashDirectory.path.toNativeUtf8().cast()); clashFFI.clash_init(_clashDirectory.path.toNativeUtf8().cast()); clashFFI.set_config(clashConf.toNativeUtf8().cast()); clashFFI.set_ext_controller(clashExtPort); if (clashFFI.parse_options() == 0) { Get.printInfo(info: "parse ok"); } Future.delayed(Duration.zero, () { initDaemon(); }); // tray show issue if (isDesktop) { trayManager.addListener(this); } // wait getx initialize // Future.delayed(const Duration(seconds: 3), () { // if (!Platform.isWindows) { // Get.find() // .showNotification("Fclash", "Is running".tr); // } // }); return this; } void getConfigs() { yamlConfigs.clear(); final entities = _clashDirectory.listSync(); for (final entity in entities) { if (entity.path.toLowerCase().endsWith('.yaml') && !yamlConfigs.contains(entity)) { yamlConfigs.add(entity); Get.printInfo(info: 'detected: ${entity.path}'); } } } Map getConnections() { final connsPtr = clashFFI.get_all_connections().cast(); String connections = connsPtr.toDartString(); // malloc.free(connsPtr); return json.decode(connections); } void closeAllConnections() { clashFFI.close_all_connections(); } bool closeConnection(String connectionId) { final id = connectionId.toNativeUtf8().cast(); return clashFFI.close_connection(id) == 1; } void getCurrentClashConfig() { final configPtr = clashFFI.get_configs().cast(); final jsondata = json.decode(configPtr.toDartString()); final data = JsonMapper.deserialize(jsondata); configEntity.value = data; // malloc.free(configPtr); } Future chageProxyConfig() async { final clashConf = p.join(_clashDirectory.path, proxyYaml.value); final f = File(clashConf); if (f.existsSync() && await changeYaml(f)) { // set subscription // await SpUtil.setData('profile_$name', url); // await reload(); } } String generateRules() { return ''' rules: - GEOSITE,cn,DIRECT - GEOIP,CN,DIRECT - MATCH,proxy '''; } Future makeClash(List nodeModes) async { var proxies = nodeModes.map(nodeToYaml).toList(); var config = ''' port: 7891 socks-port: 7890 redir-port: 7893 allow-lan: true mode: rule log-level: info ipv6: false unified-delay: false geodata-mode: true tcp-concurrent: false find-process-mode: strict global-client-fingerprint: chrome external-controller: 0.0.0.0:9090 proxies: \n${proxies.join('\n')} proxy-groups: - name: proxy type: select proxies: ${nodeModes.map((node) => '- ${node.name}').join('\n ')} ${generateRules()} '''; proxyYamlCurrent.value = config; final clashConf = p.join(_clashDirectory.path, proxyYaml.value); final configF = File(clashConf); await configF.writeAsBytes(utf8.encode(config)); } String nodeToYaml(NodeMode node) { const prefix = ' '; // 两个空格的缩进 switch (node.type) { case 'trojan': return '''$prefix- { name: ${node.name}, type: ${node.type}, server: ${node.host}, port: ${node.port}, password: ${node.passwd}, udp: 1 }'''; case 'shadowsocks': return '''$prefix- { name: ${node.name}, type: ss, server: ${node.host}, port: ${node.port}, password: ${node.passwd}, cipher: ${node.method}, udp: 1 }'''; case 'v2ray': final type = (node.vless == 1) ? 'vless' : 'vmess'; if (type == 'vless') { return '''$prefix- { name: ${node.name}, type: $type, server: ${node.host}, port: ${node.port}, uuid: ${node.uuid}, alterId: ${node.v2AlterId}, udp: 1, flow: xtls-rprx-vision, servername: www.amazon.com, tls: true, reality-opts: { public-key: ${node.vlessPulkey} } }'''; } else { return '''$prefix- { name: ${node.name}, type: $type, server: ${node.host}, port: ${node.port}, uuid: ${node.uuid}, alterId: ${node.v2AlterId}, cipher: ${node.method}, udp: 1 }'''; } default: return ''; } } Future reload() async { // get configs getConfigs(); getCurrentClashConfig(); // proxies getProxies(); updateTray(); } void initDaemon() async { printInfo(info: 'init clash service'); // wait for online // while (!await isRunning()) { // printInfo(info: 'waiting online status'); // await Future.delayed(const Duration(milliseconds: 500)); // } // get traffic // Timer.periodic(const Duration(seconds: 1), (t) { // final trafficPtr = clashFFI.get_traffic().cast(); // final traffic = trafficPtr.toDartString(); // if (kDebugMode) { // debugPrint(traffic); // } // try { // final trafficJson = jsonDecode(traffic); // uploadRate.value = trafficJson['Up'].toDouble() / 1024; // KB // downRate.value = trafficJson['Down'].toDouble() / 1024; // KB // // fix: 只有KDE不会导致Tray自动消失 // // final desktop = Platform.environment['XDG_CURRENT_DESKTOP']; // // updateTray(); // } catch (e) { // Get.printError(info: '$e'); // } // // malloc.free(trafficPtr); // }); // // system proxy // // listen port await reload(); // checkPort(); // if (isSystemProxy()) { // setSystemProxy(); // } } @override void onClose() { closeClashDaemon(); super.onClose(); } Future closeClashDaemon() async { Get.printInfo(info: 'fclash: closing daemon'); // double check // stopClashSubP(); if (isSystemProxy()) { // just clear system proxy await clearSystemProxy(permanent: false); } await _clashLock?.unlock(); } Future setSystemProxy() async { if (isDesktop) { if (configEntity.value != null) { final entity = configEntity.value!; if (entity.port != 0) { await Future.wait([ proxyManager.setAsSystemProxy( ProxyTypes.http, '127.0.0.1', entity.port!), proxyManager.setAsSystemProxy( ProxyTypes.https, '127.0.0.1', entity.port!) ]); debugPrint("set http"); } if (entity.socksPort != 0 && !Platform.isWindows) { debugPrint("set socks"); await proxyManager.setAsSystemProxy( ProxyTypes.socks, '127.0.0.1', entity.socksPort!); } await setIsSystemProxy(true); } } else { if (configEntity.value != null) { final entity = configEntity.value!; if (entity.port != 0) { // await mobileChannel // .invokeMethod("SetHttpPort", {"port": entity.port}); } // mobileChannel.invokeMethod("StartProxy"); await setIsSystemProxy(true); } // await Clipboard.setData( // ClipboardData(text: "${configEntity.value?.port}")); // final dialog = BrnDialog( // titleText: "请手动设置代理", // messageText: // "端口号已复制。请进入已连接WiFi的详情设置,将代理设置为手动,主机名填写127.0.0.1,端口填写${configEntity.value?.port},然后返回点击已完成即可", // actionsText: ["取消", "已完成", "去设置填写"], // indexedActionCallback: (index) async { // if (index == 0) { // if (Get.isOverlaysOpen) { // Get.back(); // } // } else if (index == 1) { // final proxy = await SystemProxy.getProxySettings(); // if (proxy != null) { // if (proxy["host"] == "127.0.0.1" && // int.parse(proxy["port"].toString()) == // configEntity.value?.port) { // Future.delayed(Duration.zero, () { // if (Get.overlayContext != null) { // BrnToast.show("设置成功", Get.overlayContext!); // setIsSystemProxy(true); // } // }); // if (Get.isOverlaysOpen) { // Get.back(); // } // } // } else { // Future.delayed(Duration.zero, () { // if (Get.overlayContext != null) { // BrnToast.show("好像未完成设置哦", Get.overlayContext!); // } // }); // } // } else { // Future.delayed(Duration.zero, () { // BrnToast.show("端口号已复制", Get.context!); // }); // await OpenSettings.openWIFISetting(); // } // }, // ); // Get.dialog(dialog); } } Future clearSystemProxy({bool permanent = true}) async { if (isDesktop) { await proxyManager.cleanSystemProxy(); if (permanent) { await setIsSystemProxy(false); } } else { //mobileChannel.invokeMethod("StopProxy"); await setIsSystemProxy(false); // final dialog = BrnDialog( // titleText: "请手动设置代理", // messageText: "请进入已连接WiFi的详情设置,将代理设置为无", // actionsText: ["取消", "已完成", "去设置清除"], // indexedActionCallback: (index) async { // if (index == 0) { // if (Get.isOverlaysOpen) { // Get.back(); // } // } else if (index == 1) { // final proxy = await SystemProxy.getProxySettings(); // if (proxy != null) { // Future.delayed(Duration.zero, () { // if (Get.overlayContext != null) { // BrnToast.show("好像没有清除成功哦,当前代理${proxy}", Get.overlayContext!); // } // }); // } else { // Future.delayed(Duration.zero, () { // if (Get.overlayContext != null) { // BrnToast.show("清除成功", Get.overlayContext!); // } // setIsSystemProxy(false); // if (Get.isOverlaysOpen) { // Get.back(); // } // }); // } // } else { // OpenSettings.openWIFISetting().then((_) async { // final proxy = await SystemProxy.getProxySettings(); // debugPrint("$proxy"); // }); // } // }, // ); // Get.dialog(dialog); } } void getProxies() { final proxiesPtr = clashFFI.get_proxies().cast(); proxies.value = json.decode(proxiesPtr.toDartString()); } bool isSystemProxy() { return SpUtil.getData('system_proxy', defValue: false); } Future setIsSystemProxy(bool proxy) { isSystemProxyObs.value = proxy; return SpUtil.setData('system_proxy', proxy); } void checkPort() { if (configEntity.value != null) { if (configEntity.value!.port == 0) { changeConfigField('port', initializedHttpPort); } if (configEntity.value!.mixedPort == 0) { changeConfigField('mixed-port', initializedMixedPort); } if (configEntity.value!.socksPort == 0) { changeConfigField('socks-port', initializedSockPort); } updateTray(); } } //切换配置 bool changeConfigField(String field, dynamic value) { try { int ret = clashFFI.change_config_field( json.encode({field: value}).toNativeUtf8().cast()); return ret == 0; } finally { getCurrentClashConfig(); if (field.endsWith("port") && isSystemProxy()) { setSystemProxy(); } } } Future _acquireLock(Directory clashDirectory) async { final path = p.join(clashDirectory.path, "fclash.lock"); final lockFile = File(path); if (!lockFile.existsSync()) { lockFile.createSync(recursive: true); } try { _clashLock = await lockFile.open(mode: FileMode.write); await _clashLock?.lock(); } catch (e) { // if (!Platform.isWindows) { // await Get.find() // .showNotification("Fclash", "Already running, Now exit.".tr); // } exit(0); } } ReceivePort? _logReceivePort; void startLogging() { _logReceivePort?.close(); _logReceivePort = ReceivePort(); logStream = _logReceivePort!.asBroadcastStream(); if (kDebugMode) { logStream?.listen((event) { debugPrint("LOG: ${event}"); }); } final nativePort = _logReceivePort!.sendPort.nativePort; debugPrint("port: $nativePort"); clashFFI.start_log(nativePort); } void stopLog() { logStream = null; clashFFI.stop_log(); } void updateTray() { if (!isDesktop) { return; } final stringList = List.empty(growable: true); // yaml stringList .add(MenuItem(label: "profile: ${currentYaml.value}", disabled: true)); if (proxies['proxies'] != null) { Map m = proxies['proxies']; m.removeWhere((key, value) => value['type'] != "Selector"); var cnt = 0; for (final k in m.keys) { if (cnt >= ClashService.MAX_ENTRIES) { stringList.add(MenuItem(label: "...", disabled: true)); break; } stringList.add( MenuItem(label: "${m[k]['name']}: ${m[k]['now']}", disabled: true)); cnt += 1; } } // port if (configEntity.value != null) { stringList.add( MenuItem(label: 'http: ${configEntity.value?.port}', disabled: true)); stringList.add(MenuItem( label: 'socks: ${configEntity.value?.socksPort}', disabled: true)); } // system proxy stringList.add(MenuItem.separator()); if (!isSystemProxy()) { stringList .add(MenuItem(label: "Not system proxy yet.".tr, disabled: true)); stringList.add(MenuItem( label: "Set as system proxy".tr, toolTip: "click to set fclash as system proxy".tr, key: ACTION_SET_SYSTEM_PROXY)); } else { stringList.add(MenuItem(label: "System proxy now.".tr, disabled: true)); stringList.add(MenuItem( label: "Unset system proxy".tr, toolTip: "click to reset system proxy", key: ACTION_UNSET_SYSTEM_PROXY)); stringList.add(MenuItem.separator()); } initAppTray(details: stringList, isUpdate: true); } Future _changeConfig(FileSystemEntity config) async { // check if it has `rule-set`, and try to convert it final content = await convertConfig(await File(config.path).readAsString()) .catchError((e) { printError(info: e); }); if (content.isNotEmpty) { await File(config.path).writeAsString(content); } // judge valid if (clashFFI.is_config_valid(config.path.toNativeUtf8().cast()) == 0) { final resp = await Request.dioClient.put('/configs', queryParameters: {"force": false}, data: {"path": config.path}); Get.printInfo(info: 'config changed ret: ${resp.statusCode}'); currentYaml.value = basename(config.path); SpUtil.setData('yaml', currentYaml.value); return resp.statusCode == 204; } else { Future.delayed(Duration.zero, () { Get.defaultDialog( middleText: 'not a valid config file'.tr, onConfirm: () { Get.back(); }); }); config.delete(); return false; } } Future addProfile(String name, String url) async { final configName = '$name.yaml'; final newProfilePath = join(_clashDirectory.path, configName); try { final uri = Uri.tryParse(url); if (uri == null) { return false; } final resp = await Dio(BaseOptions( headers: {'User-Agent': 'clash.meta'}, sendTimeout: 15000, receiveTimeout: 15000)) .downloadUri(uri, newProfilePath, onReceiveProgress: (i, t) { Get.printInfo(info: "$i/$t"); }); return resp.statusCode == 200; } catch (e) { //BrnToast.show("Error: ${e}", Get.context!); } finally { final f = File(newProfilePath); if (f.existsSync() && await changeYaml(f)) { // set subscription await SpUtil.setData('profile_$name', url); return true; } return false; } } Future deleteProfile(FileSystemEntity config) async { if (config.existsSync()) { config.deleteSync(); await SpUtil.remove('profile_${basename(config.path)}'); reload(); return true; } else { return false; } } //切换配置文件 Future changeYaml(FileSystemEntity config) async { try { if (await config.exists()) { return await _changeConfig(config); } else { return false; } } finally { reload(); } } bool changeProxy(String selectName, String proxyName) { final ret = clashFFI.change_proxy( selectName.toNativeUtf8().cast(), proxyName.toNativeUtf8().cast()); if (ret == 0) { reload(); } return ret == 0; } bool isHideWindowWhenStart() { return SpUtil.getData('boot_window_hide', defValue: false); } void handleSignal() { StreamSubscription? subTerm; subTerm = ProcessSignal.sigterm.watch().listen((event) { subTerm?.cancel(); // _clashProcess?.kill(); }); } } Future convertConfig(String content) async { return ""; }