commit cffd27dcbea079a9c534781caa228710982d7b43 Author: 会做饭的二哈 Date: Sat Mar 15 23:25:22 2025 +0800 first commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29a3a50 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..77d11fd --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + - platform: windows + create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5664d3a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,58 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Flutter Run", + "request": "launch", + "type": "dart", + "program": "lib/main.dart", + "args": ["run"] + }, + { + "name": "fln2n", + "request": "launch", + "type": "dart" + }, + { + "name": "fln2n (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "fln2n (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "rust_builder", + "cwd": "rust_builder", + "request": "launch", + "type": "dart" + }, + { + "name": "rust_builder (profile mode)", + "cwd": "rust_builder", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "rust_builder (release mode)", + "cwd": "rust_builder", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "build_tool", + "cwd": "rust_builder\\cargokit\\build_tool", + "request": "launch", + "type": "dart" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3cd0822 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# fltier + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..ed0980a --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,31 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +analyzer: + errors: + file_names: ignore +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/assets/dlls/Packet.dll b/assets/dlls/Packet.dll new file mode 100644 index 0000000..7e1bf12 Binary files /dev/null and b/assets/dlls/Packet.dll differ diff --git a/assets/dlls/wintun.dll b/assets/dlls/wintun.dll new file mode 100644 index 0000000..aee04e7 Binary files /dev/null and b/assets/dlls/wintun.dll differ diff --git a/assets/fonts/MiSans-Demibold.ttf b/assets/fonts/MiSans-Demibold.ttf new file mode 100644 index 0000000..39d5887 Binary files /dev/null and b/assets/fonts/MiSans-Demibold.ttf differ diff --git a/assets/fonts/MiSans-Regular.ttf b/assets/fonts/MiSans-Regular.ttf new file mode 100644 index 0000000..9cbfff5 Binary files /dev/null and b/assets/fonts/MiSans-Regular.ttf differ diff --git a/assets/icon.ico b/assets/icon.ico new file mode 100644 index 0000000..a3388e3 Binary files /dev/null and b/assets/icon.ico differ diff --git a/assets/tray_icon.png b/assets/tray_icon.png new file mode 100644 index 0000000..84245bb Binary files /dev/null and b/assets/tray_icon.png differ diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/flutter_rust_bridge.yaml b/flutter_rust_bridge.yaml new file mode 100644 index 0000000..7ba9469 --- /dev/null +++ b/flutter_rust_bridge.yaml @@ -0,0 +1,4 @@ +rust_input: crate::api +rust_root: rust/ +dart_output: lib/src/rust +default_dart_async: true \ No newline at end of file diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..223821b --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,156 @@ +// 导入必要的Flutter包和自定义模块 +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:system_tray/system_tray.dart'; +import 'dart:io'; +import 'screens/主屏幕.dart'; +import 'config/主题配置.dart'; +import 'config/app_config.dart'; + +// 定义应用程序的主要StatefulWidget +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +// 应用程序的状态管理类 +class _MyAppState extends State { + // 主题相关的状态变量 + late ThemeMode _themeMode; + late bool useMaterial3; + late Color _seedColor; + int _currentIndex = 0; + + // 系统托盘相关变量 + final SystemTray _systemTray = SystemTray(); + final AppWindow _appWindow = AppWindow(); + + @override + void initState() { + super.initState(); + // 从配置中加载设置 + final config = AppConfig(); + _themeMode = config.themeMode; + useMaterial3 = true; + _seedColor = config.seedColor; + + // 初始化系统托盘 + initSystemTray(); + } + + // 初始化系统托盘 + Future initSystemTray() async { + // 设置托盘图标 + String path = 'assets/icon.ico'; + + // 初始化托盘 + await _systemTray.initSystemTray( + title: "ASTRAL", + iconPath: path, + ); + + // 设置托盘菜单 + final Menu menu = Menu(); + await menu.buildFrom([ + MenuItemLabel(label: '打开应用', onClicked: (menuItem) => _appWindow.show()), + MenuItemLabel(label: '退出', onClicked: (menuItem) => exit(0)), + ]); + + // 设置托盘菜单 + await _systemTray.setContextMenu(menu); + + // 设置托盘点击事件 + _systemTray.registerSystemTrayEventHandler((eventName) { + if (eventName == kSystemTrayEventClick) { + _appWindow.show(); + } else if (eventName == kSystemTrayEventRightClick) { + _systemTray.popUpContextMenu(); + } + }); + } + + // 切换主题模式的方法 + void toggleThemeMode() { + setState(() { + if (_themeMode == ThemeMode.light) { + _themeMode = ThemeMode.dark; + } else if (_themeMode == ThemeMode.dark) { + _themeMode = ThemeMode.system; + } else { + _themeMode = ThemeMode.light; + } + AppConfig().setThemeMode(_themeMode); + }); + } + + // 更改主题色的方法 + void changeSeedColor(Color color) { + // 使用 Future.microtask 延迟状态更新,避免在当前帧中触发重建 + setState(() { + _seedColor = color; + AppConfig().setSeedColor(color); + }); + } + + // 更改底部导航栏选中索引的方法 + void changeIndex(int index) { + setState(() { + _currentIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + // 返回应用程序的根Widget + return MaterialApp( + debugShowCheckedModeBanner: false, // 隐藏调试标签 + localizationsDelegates: const [ + // 添加国际化支持 + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + // Insert this line + supportedLocales: const [Locale("zh", "CN"), Locale("en", "US")], + theme: ThemeConfig.getLightTheme( + useMaterial3: useMaterial3, + seedColor: _seedColor, + ).copyWith( + textTheme: Typography.material2021().black.apply( + fontFamily: 'MiSans', + ), + primaryTextTheme: Typography.material2021().black.apply( + fontFamily: 'MiSans', + )), + darkTheme: ThemeConfig.getDarkTheme( + useMaterial3: useMaterial3, + seedColor: _seedColor, + ).copyWith( + textTheme: Typography.material2021().white.apply( + fontFamily: 'MiSans', + ), + primaryTextTheme: Typography.material2021().white.apply( + fontFamily: 'MiSans', + ), + ), + themeMode: _themeMode, // 设置当前主题模式 + home: MainScreen( + // 设置主屏幕 + // 切换主题模式的回调函数 + toggleThemeMode: toggleThemeMode, + // 更改主题色的回调函数 + changeSeedColor: changeSeedColor, + // 当前主题模式状态 + currentThemeMode: _themeMode, + // 当前选中的底部导航栏索引 + currentIndex: _currentIndex, + // 更改底部导航栏索引的回调函数 + changeIndex: changeIndex, + // 当前主题色 + seedColor: _seedColor, + ), + ); + } +} diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart new file mode 100644 index 0000000..755ea5e --- /dev/null +++ b/lib/config/app_config.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:io'; // 添加 dart:io 导入以使用 Platform 类 + +class AppConfig { + static final AppConfig _instance = AppConfig._internal(); + static late SharedPreferences _prefs; + + factory AppConfig() { + return _instance; + } + + AppConfig._internal(); + + // 初始化配置 + static Future init() async { + _prefs = await SharedPreferences.getInstance(); + } + + // 主题设置 + static const String _keyThemeMode = 'themeMode'; + ThemeMode get themeMode { + final String? value = _prefs.getString(_keyThemeMode); + return ThemeMode.values.firstWhere( + (mode) => mode.toString() == value, + orElse: () => ThemeMode.system, + ); + } + + Future setThemeMode(ThemeMode mode) async { + await _prefs.setString(_keyThemeMode, mode.toString()); + } + + // 主题色设置 + static const String _keySeedColor = 'seedColor'; + Color get seedColor { + final int? value = _prefs.getInt(_keySeedColor); + return value != null ? Color(value) : Colors.blue; + } + + Future setSeedColor(Color color) async { + await _prefs.setInt(_keySeedColor, color.value); + } + + // 服务器列表设置 + static const String _keyServerList = 'serverList'; + List get serverList { + final List? value = _prefs.getStringList(_keyServerList); + return value?.isNotEmpty == true ? value! : ['public.easytier.net:11010']; + } + + Future setServerList(List servers) async { + await _prefs.setStringList(_keyServerList, servers); + } + + // 当前选中的服务器设置 + static const String _keyCurrentServer = 'currentServer'; + String get currentServer { + return _prefs.getString(_keyCurrentServer) ?? 'public.easytier.net:11010'; + } + + Future setCurrentServer(String server) async { + await _prefs.setString(_keyCurrentServer, server); + } + + // 房间名设置 + static const String _keyRoomName = 'roomName'; + String get roomName { + return _prefs.getString(_keyRoomName) ?? 'kevin'; + } + + Future setRoomName(String name) async { + await _prefs.setString(_keyRoomName, name); + } + + // 房间密码设置 + static const String _keyRoomPassword = 'roomPassword'; + String get roomPassword { + return _prefs.getString(_keyRoomPassword) ?? 'kevin'; + } + + Future setRoomPassword(String password) async { + await _prefs.setString(_keyRoomPassword, password); + } + + // 用户名设置 + static const String _keyUsername = 'username'; + String get username { + return _prefs.getString(_keyUsername) ?? Platform.localHostname; + } + + Future setUsername(String name) async { + await _prefs.setString(_keyUsername, name); + } + + // 虚拟IP设置 + static const String _keyVirtualIP = 'virtualIP'; + String get virtualIP { + return _prefs.getString(_keyVirtualIP) ?? ''; + } + + // 关闭按钮进入托盘 + static const String _keyCloseToTray = 'closeToTray'; + bool get closeToTray { + return _prefs.getBool(_keyCloseToTray) ?? true; + } + + Future setCloseToTray(bool enabled) async { + await _prefs.setBool(_keyCloseToTray, enabled); + } + + Future setVirtualIP(String ip) async { + await _prefs.setString(_keyVirtualIP, ip); + } + + // 动态获取IP设置 + static const String _keyDynamicIP = 'dynamicIP'; + bool get dynamicIP { + return _prefs.getBool(_keyDynamicIP) ?? true; + } + + Future setDynamicIP(bool enabled) async { + await _prefs.setBool(_keyDynamicIP, enabled); + } +} diff --git a/lib/config/主题配置.dart b/lib/config/主题配置.dart new file mode 100644 index 0000000..a4b2478 --- /dev/null +++ b/lib/config/主题配置.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +// 应用主题配置 +class ThemeConfig { + // 默认使用Material 3 + static const bool defaultUseMaterial3 = true; + + // 默认种子颜色 + static const Color defaultSeedColor = Colors.blue; + + // 默认主题模式 + static const ThemeMode defaultThemeMode = ThemeMode.system; + + // 获取亮色主题 + static ThemeData getLightTheme({ + required bool useMaterial3, + required Color seedColor, + }) { + return ThemeData( + useMaterial3: useMaterial3, + colorSchemeSeed: seedColor, + brightness: Brightness.light, + ); + } + + // 获取暗色主题 + static ThemeData getDarkTheme({ + required bool useMaterial3, + required Color seedColor, + }) { + return ThemeData( + useMaterial3: useMaterial3, + colorSchemeSeed: seedColor, + brightness: Brightness.dark, + ); + } +} diff --git a/lib/config/导航配置.dart b/lib/config/导航配置.dart new file mode 100644 index 0000000..502b9e6 --- /dev/null +++ b/lib/config/导航配置.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import '../screens/首页.dart'; +import '../screens/设置.dart'; +import '../screens/关于.dart'; +import '../screens/房间.dart'; + +class NavItem { + final String label; + final IconData icon; + final IconData selectedIcon; + final Widget Function() pageBuilder; // 修改为函数,延迟创建页面 + + const NavItem({ + required this.label, + required this.icon, + required this.selectedIcon, + required this.pageBuilder, + }); +} + +class NavigationConfig { + // 保存已创建的页面实例 + static final Map _pageInstances = {}; + + static List getNavItems({ + required Function toggleThemeMode, + required Function(Color) changeSeedColor, + required ThemeMode currentThemeMode, + }) { + return [ + NavItem( + label: '首页', + icon: Icons.home_outlined, + selectedIcon: Icons.home, + pageBuilder: () => _getOrCreatePage( + 0, + () => HomePage( + toggleThemeMode: toggleThemeMode, + changeSeedColor: changeSeedColor, + currentThemeMode: currentThemeMode, + )), + ), + NavItem( + label: '房间', + icon: Icons.room_outlined, + selectedIcon: Icons.room, + pageBuilder: () => _getOrCreatePage(1, () => const RoomPage()), + ), + NavItem( + label: '设置', + icon: Icons.settings_outlined, + selectedIcon: Icons.settings, + pageBuilder: () => _getOrCreatePage(2, () => const SettingsPage()), + ), + NavItem( + label: '关于', + icon: Icons.info_outlined, + selectedIcon: Icons.info, + pageBuilder: () => _getOrCreatePage(3, () => const InfoPage()), + ), + ]; + } + + // 获取或创建页面实例 + static Widget _getOrCreatePage(int index, Widget Function() creator) { + if (!_pageInstances.containsKey(index)) { + _pageInstances[index] = creator(); + } + return _pageInstances[index]!; + } + + // 清除缓存的页面实例(在需要重新创建页面时调用,如主题变更) + static void clearPageInstances() { + _pageInstances.clear(); + } +} diff --git a/lib/config/窗口配置.dart b/lib/config/窗口配置.dart new file mode 100644 index 0000000..40fe349 --- /dev/null +++ b/lib/config/窗口配置.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:window_manager/window_manager.dart'; + +Future setupWindow() async { + WindowOptions windowOptions = const WindowOptions( + size: Size(850, 520), + minimumSize: Size(300, 300), + center: true, + backgroundColor: Colors.transparent, + skipTaskbar: false, + titleBarStyle: TitleBarStyle.hidden, // 隐藏标题栏 + ); + + await windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }); +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..e22ac26 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:ASTRAL/src/rust/frb_generated.dart'; +import 'package:window_manager/window_manager.dart'; +import 'app.dart'; +import 'config/窗口配置.dart'; +import 'config/app_config.dart'; +import 'utils/状态.dart'; +import 'package:provider/provider.dart'; // 添加这一行 +import 'package:tray_manager/tray_manager.dart'; +import 'package:ASTRAL/utils/app_info.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + // 初始化应用信息 + await AppInfoUtil.init(); + await windowManager.ensureInitialized(); + // 设置窗口属性 + await setupWindow(); + await AppConfig.init(); + // 初始化应用信息 + await RustLib.init(); + runApp( + ChangeNotifierProvider( + create: (context) => KM(), + child: const MyApp(), + ), + ); +} diff --git a/lib/screens/主屏幕.dart b/lib/screens/主屏幕.dart new file mode 100644 index 0000000..4c4a86c --- /dev/null +++ b/lib/screens/主屏幕.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; +import 'package:window_manager/window_manager.dart'; +import '../widgets/窗口控制按钮.dart'; +import '../widgets/主题选择器.dart'; +import '../utils/主题工具.dart'; +import '../config/导航配置.dart'; + +class MainScreen extends StatefulWidget { + final Function toggleThemeMode; + final Function(Color) changeSeedColor; + final ThemeMode currentThemeMode; + final int currentIndex; + final Function(int) changeIndex; + final Color seedColor; + + const MainScreen({ + super.key, + required this.toggleThemeMode, + required this.changeSeedColor, + required this.currentThemeMode, + required this.currentIndex, + required this.changeIndex, + required this.seedColor, + }); + + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends State + with SingleTickerProviderStateMixin { + // 缓存所有页面 + late List _pages; + late List _navItems; + late AnimationController _titleAnimationController; + + @override + void initState() { + super.initState(); + _initPages(); + + // 初始化标题动画控制器 + _titleAnimationController = AnimationController( + duration: const Duration(seconds: 3), + vsync: this, + )..repeat(); + } + + @override + void dispose() { + _titleAnimationController.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(MainScreen oldWidget) { + super.didUpdateWidget(oldWidget); + // 当主题模式或种子颜色变化时,重新初始化页面 + if (oldWidget.currentThemeMode != widget.currentThemeMode || + oldWidget.seedColor != widget.seedColor) { + NavigationConfig.clearPageInstances(); + _initPages(); + } + } + + void _initPages() { + _navItems = NavigationConfig.getNavItems( + toggleThemeMode: widget.toggleThemeMode, + changeSeedColor: widget.changeSeedColor, + currentThemeMode: widget.currentThemeMode, + ); + + // 预先创建所有页面 + _pages = _navItems.map((item) => item.pageBuilder()).toList(); + } + + // 将侧边栏构建方法整合到MainScreen类中 + Widget buildSidebar(BuildContext context) { + return Material( + color: Theme.of(context).colorScheme.surface, + child: ListView( + padding: EdgeInsets.zero, + children: List.generate( + _navItems.length, + (index) => ListTile( + leading: Icon(_navItems[index].icon), + title: Text(_navItems[index].label), + selected: widget.currentIndex == index, + onTap: () => widget.changeIndex(index), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + // 获取屏幕宽度,用于判断使用哪种导航方式 + final double screenWidth = MediaQuery.of(context).size.width; + // 设置一个阈值,当宽度大于此值时使用侧边栏 + final bool useSidebar = screenWidth > 600; + + // 创建一个共享的 IndexedStack 实例 + final indexedStack = IndexedStack( + index: widget.currentIndex, + children: _pages, + ); + + return Scaffold( + appBar: AppBar( + title: ShaderMask( + shaderCallback: (bounds) => LinearGradient( + colors: [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.tertiary, + ], + ).createShader(bounds), + child: const Text( + 'ASTRAL', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), + ), + ), + elevation: 2, + scrolledUnderElevation: 0, // 修改为0,防止滚动时出现阴影变化 + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, // 使用更深沉的主题颜色 + foregroundColor: + Theme.of(context).colorScheme.onPrimaryContainer, // 对应的前景色 + // 设置滚动时的背景色保持一致 + flexibleSpace: GestureDetector( + behavior: HitTestBehavior.translucent, + onPanStart: (details) { + windowManager.startDragging(); + }, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primaryContainer, // 与backgroundColor保持一致 + ), + ), + ), + titleSpacing: NavigationToolbar.kMiddleSpacing, + toolbarHeight: 40, // 减小AppBar的高度 + iconTheme: const IconThemeData(size: 18), // 设置所有图标的大小 + actionsIconTheme: const IconThemeData(size: 18), // 设置操作区图标的大小 + + actions: [ + IconButton( + icon: Icon( + widget.currentThemeMode == ThemeMode.light + ? Icons.wb_sunny + : widget.currentThemeMode == ThemeMode.dark + ? Icons.nightlight_round + : Icons.auto_mode, + ), + onPressed: () => widget.toggleThemeMode(), + tooltip: getThemeModeText(widget.currentThemeMode), + padding: const EdgeInsets.all(8), // 减小按钮内边距 + ), + IconButton( + icon: const Icon(Icons.color_lens), + onPressed: () => showThemeColorPicker( + context, widget.seedColor, widget.changeSeedColor), + tooltip: '选择主题颜色', + padding: const EdgeInsets.all(8), // 减小按钮内边距 + ), + const WindowControls(), + ], + ), + // 根据屏幕宽度决定使用侧边栏还是底部导航栏 + body: useSidebar + ? Row( + children: [ + // 固定显示的侧边栏 + SizedBox( + width: 120, + child: buildSidebar(context), + ), + const VerticalDivider(width: 1, thickness: 1), + // 使用共享的 IndexedStack + Expanded( + child: indexedStack, + ), + ], + ) + : indexedStack, + // 只在窄屏幕时显示底部导航栏 + bottomNavigationBar: useSidebar + ? null + : NavigationBar( + selectedIndex: widget.currentIndex, + onDestinationSelected: (index) => widget.changeIndex(index), + destinations: List.generate( + _navItems.length, + (index) => NavigationDestination( + icon: Icon(_navItems[index].icon), + selectedIcon: Icon(_navItems[index].selectedIcon), + label: _navItems[index].label, + ), + ), + ), + ); + } +} diff --git a/lib/screens/关于.dart b/lib/screens/关于.dart new file mode 100644 index 0000000..01aa2c1 --- /dev/null +++ b/lib/screens/关于.dart @@ -0,0 +1,328 @@ +// ignore_for_file: file_names + +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'dart:math' as math; +import 'package:ASTRAL/utils/app_info.dart'; + +class InfoPage extends StatefulWidget { + const InfoPage({super.key}); + + @override + State createState() => _InfoPageState(); +} + +class _InfoPageState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 10), + vsync: this, + )..repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 旋转的图标效果 + AnimatedBuilder( + animation: _controller, + builder: (_, child) { + return Transform.rotate( + angle: _controller.value * 2 * math.pi, + child: Container( + height: 110, + width: 110, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: SweepGradient( + colors: [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.secondary, + Theme.of(context).colorScheme.tertiary, + Theme.of(context).colorScheme.primary, + ], + stops: const [0.0, 0.3, 0.6, 1.0], + transform: + GradientRotation(_controller.value * 2 * math.pi), + ), + boxShadow: [ + BoxShadow( + color: Theme.of(context) + .colorScheme + .primary + .withOpacity(0.5), + blurRadius: 15, + spreadRadius: 1, + ), + ], + ), + child: Center( + child: Icon( + Icons.games, + size: 50, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), + ); + }, + ), + const SizedBox(height: 20), + // 应用名称添加渐变效果 + ShaderMask( + shaderCallback: (bounds) => LinearGradient( + colors: [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.tertiary, + ], + ).createShader(bounds), + child: Text( + 'ASTRAL', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + letterSpacing: 2.0, + ), + ), + ), + const SizedBox(height: 8), + // 版本号添加动画效果 + TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + duration: const Duration(milliseconds: 800), + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(0, 20 * (1 - value)), + child: child, + ), + ); + }, + child: Text( + AppInfoUtil.getFullVersion(), + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + const SizedBox(height: 20), + // 卡片添加动画和阴影效果 + _buildAnimatedCard( + context, + '特别鸣谢', + '特别感谢EasyTier作者所做的工作和帮助,为本项目提供了重要的技术支持。如果您有功能需求或遇到bug,欢迎加入我们的QQ群获取帮助和了解最新动态。', + Icons.favorite, + Colors.red, + delay: 200, + ), + // 合并后的卡片 + + const SizedBox(height: 30), + // 按钮添加动画效果 + TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + duration: const Duration(milliseconds: 1000), + curve: Curves.elasticOut, + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: child, + ); + }, + child: ElevatedButton.icon( + icon: const Icon(Icons.group_add), + label: const Text('加入QQ群'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + elevation: 5, + ), + onPressed: () async { + final url = 'https://qm.qq.com/q/ErscyNPTzO'; + if (await canLaunchUrl(Uri.parse(url))) { + await launchUrl(Uri.parse(url)); + } else { + // 无法打开链接时显示提示 + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('无法打开QQ群链接')), + ); + } + } + }, + ), + ), + const SizedBox(height: 20), + // 添加版权信息 + TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + duration: const Duration(milliseconds: 1200), + builder: (context, value, child) { + return Opacity( + opacity: value, + child: child, + ); + }, + child: Text( + '© ${DateTime.now().year} ASTRAL Team', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.6), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildAnimatedCard(BuildContext context, String title, String content, + IconData icon, Color iconColor, + {int delay = 0}) { + // 根据当前主题调整颜色 + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + final cardColor = Theme.of(context).cardColor; + final textColor = Theme.of(context).textTheme.bodyMedium?.color; + + return TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + duration: Duration(milliseconds: 800 + delay), + curve: Curves.easeOutBack, + builder: (context, value, child) { + // 确保 opacity 值在有效范围内 (0.0 到 1.0) + final safeOpacity = value.clamp(0.0, 1.0); + return Opacity( + opacity: safeOpacity, + child: Transform.translate( + offset: Offset(100 * (1 - value), 0), + child: child, + ), + ); + }, + child: Card( + elevation: 8, + shadowColor: iconColor.withOpacity(isDarkMode ? 0.3 : 0.4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + cardColor, + isDarkMode + ? cardColor.withOpacity(0.9).withBlue(cardColor.blue + 5) + : iconColor.withOpacity(0.05), + cardColor, + ], + stops: const [0.0, 0.5, 1.0], + ), + boxShadow: [ + BoxShadow( + color: iconColor.withOpacity(isDarkMode ? 0.05 : 0.1), + blurRadius: 10, + spreadRadius: -5, + offset: const Offset(0, 5), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 左侧图标 - 适配深色模式 + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: iconColor.withOpacity(isDarkMode ? 0.2 : 0.15), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: iconColor.withOpacity(isDarkMode ? 0.15 : 0.2), + blurRadius: isDarkMode ? 6 : 8, + spreadRadius: isDarkMode ? 0 : 1, + ), + ], + gradient: RadialGradient( + colors: [ + iconColor.withOpacity(isDarkMode ? 0.8 : 0.7), + iconColor.withOpacity(isDarkMode ? 0.2 : 0.1), + ], + stops: const [0.0, 1.0], + radius: 0.8, + ), + ), + child: Icon( + icon, + color: Colors.white, + size: 30, + ), + ), + const SizedBox(width: 16), + // 右侧内容 - 适配深色模式 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + color: + iconColor.withOpacity(isDarkMode ? 0.9 : 0.8), + ), + ), + const SizedBox(height: 8), + Divider( + color: iconColor.withOpacity(isDarkMode ? 0.4 : 0.3), + thickness: 1.5, + endIndent: 60, + ), + const SizedBox(height: 8), + Text( + content, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + height: 1.5, + letterSpacing: 0.5, + color: textColor + ?.withOpacity(isDarkMode ? 0.9 : 1.0), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/房间.dart b/lib/screens/房间.dart new file mode 100644 index 0000000..d2c1d8e --- /dev/null +++ b/lib/screens/房间.dart @@ -0,0 +1,704 @@ +// 导入必要的包 +import 'dart:convert'; +import 'package:flutter/services.dart'; // 添加这一行导入剪贴板服务 + +import 'package:ASTRAL/src/rust/api/simple.dart'; +import 'package:ASTRAL/utils/%E7%8A%B6%E6%80%81.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../widgets/卡片.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; + +/// 玩家信息模型类 +class PlayerInfo { + final String name; + final String ip; + final int latency; // 延迟(ms) + final String connectionType; // 连接类型:直链、中转、本机 + final int uploadSpeed; // 上传速度(KB/s) + final int downloadSpeed; // 下载速度(KB/s) + final int sentPackets; // 发送包数量 + final int receivedPackets; // 接收包数量 + final double packetLossRate; // 丢包率(%) + final String etVersion; // ET版本 + + PlayerInfo({ + required this.name, + required this.ip, + required this.latency, + required this.connectionType, + required this.uploadSpeed, + required this.downloadSpeed, + required this.sentPackets, + required this.receivedPackets, + required this.packetLossRate, + required this.etVersion, + }); +} + +/// 房间页面组件 +/// 用于显示所有玩家的信息 +class RoomPage extends StatefulWidget { + const RoomPage({super.key}); + + @override + State createState() => _RoomPageState(); +} + +class _RoomPageState extends State { + List players = []; + bool isLoading = true; + // 添加布局类型状态变量 + bool isGridLayout = true; // 默认使用网格布局 + + @override + void initState() { + super.initState(); + isLoading = true; + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Scaffold( + appBar: AppBar( + title: const Text('房间成员'), + // 添加布局切换按钮 + actions: [ + IconButton( + icon: Icon(isGridLayout ? Icons.view_list : Icons.grid_view), + tooltip: isGridLayout ? '切换到列表视图' : '切换到网格视图', + onPressed: () { + setState(() { + isGridLayout = !isGridLayout; + }); + }, + ), + ], + ), + body: Consumer( + builder: (context, km, child) { + // 当 KM 更新时,这个 builder 会被重新调用 + // 异步处理数据 + _processNodeData(km); + + if (isLoading) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + color: colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + '加载玩家信息...', + style: TextStyle( + color: colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } else if (players.isEmpty) { + // 添加空数据状态显示 + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.people_outline, + size: 64, + color: colorScheme.primary.withOpacity(0.6), + ), + const SizedBox(height: 16), + Text( + '暂无房间成员', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ), + ), + const SizedBox(height: 8), + Text( + '当前没有其他玩家连接到房间', + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.7), + ), + ), + ], + ), + ); + } else { + return CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverPadding( + padding: const EdgeInsets.all(16.0), + sliver: isGridLayout + ? SliverMasonryGrid.count( + crossAxisCount: _getColumnCount(context), + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childCount: players.length, + itemBuilder: (context, index) { + return _buildPlayerCard( + players[index], colorScheme); + }, + ) + : SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: _buildPlayerListItem( + players[index], colorScheme), + ); + }, + childCount: players.length, + ), + ), + ), + ], + ); + } + }, + ), + ); + } + + int _getColumnCount(BuildContext context) { + final width = MediaQuery.of(context).size.width; + if (width < 600) { + return 1; // 手机屏幕显示1列 + } else if (width < 900) { + return 2; // 平板或小屏幕显示2列 + } else { + return 3; // 大屏幕显示3列 + } + } + + // 处理节点数据 - 合并了原来的两个相似方法 + Future _processNodeData(KM km) async { + try { + final nodes = await km.nodes; // 获取最新的节点信息 + + // 将节点数据转换为PlayerInfo对象 + List nodePlayerInfos = []; + + for (var node in nodes) { + // 计算上传下载速度和包数量总和 + int uploadSpeed = 0; + int downloadSpeed = 0; + int sentPackets = 0; + int receivedPackets = 0; + String connectionType = + _mapConnectionType(node.cost, node.ipv4, km.virtualIP); + + // 如果有连接信息,计算网络统计数据 + if (node.connections.isNotEmpty) { + for (var conn in node.connections) { + uploadSpeed += (conn.txBytes as BigInt).toInt() ~/ 1024; // 转换为KB + downloadSpeed += (conn.rxBytes as BigInt).toInt() ~/ 1024; // 转换为KB + sentPackets += conn.txPackets.toInt(); + receivedPackets += conn.rxPackets.toInt(); + } + } + + // 计算丢包率 (简单估算) + double packetLossRate = 0.0; + if (sentPackets > 0 && receivedPackets > 0) { + packetLossRate = (1.0 - (receivedPackets / sentPackets)).abs() * 100; + if (packetLossRate > 100) packetLossRate = 100.0; + packetLossRate = double.parse(packetLossRate.toStringAsFixed(1)); + } + + // 创建PlayerInfo对象 + nodePlayerInfos.add( + PlayerInfo( + name: node.hostname, + ip: node.ipv4, // 临时IP,实际应从节点信息中获取 + latency: (node.latencyMs * 1000).toInt(), // 转换为毫秒 + connectionType: connectionType, + uploadSpeed: uploadSpeed, + downloadSpeed: downloadSpeed, + sentPackets: sentPackets, + receivedPackets: receivedPackets, + packetLossRate: packetLossRate, + etVersion: node.version, // 获取版本信息 + ), + ); + } + + if (!mounted) return; // 检查组件是否仍然挂载 + + setState(() { + players = nodePlayerInfos; + isLoading = false; + }); + } catch (e) { + print("加载节点数据失败: $e"); + if (mounted) { + setState(() { + isLoading = false; + }); + } + } + } + + // 构建玩家信息卡片 + Widget _buildPlayerCard(PlayerInfo player, ColorScheme colorScheme) { + // 根据延迟值确定颜色 + Color latencyColor = _getLatencyColor(player.latency); + + // 根据连接类型选择图标 + IconData connectionIcon = _getConnectionIcon(player.connectionType); + + return FloatingCard( + colorScheme: colorScheme, + maxWidth: 600, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 玩家名称和连接类型 + Row( + children: [ + Icon(Icons.person, color: colorScheme.primary, size: 22), + const SizedBox(width: 8), + Expanded( + child: Text( + player.name, + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _getConnectionTypeColor( + player.connectionType, colorScheme), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + connectionIcon, + size: 14, + color: Colors.white, + ), + const SizedBox(width: 4), + Text( + player.connectionType, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + + // IP地址 + _buildInfoRow( + Icons.lan, + 'IP地址', + player.ip, + colorScheme, + showCopyButton: true, + ), + const SizedBox(height: 12), + + // 延迟信息 + _buildInfoRow( + Icons.speed, + '延迟', + '${player.latency} ms', + colorScheme, + valueColor: latencyColor, + ), + const SizedBox(height: 12), + + // ET版本 + _buildInfoRow( + Icons.memory, + 'ET版本', + player.etVersion, + colorScheme, + ), + + const Divider(height: 24), + + // 网络数据部分标题 + Row( + children: [ + Icon(Icons.data_usage, color: colorScheme.primary, size: 18), + const SizedBox(width: 8), + Text( + '网络数据', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: colorScheme.primary, + ), + ), + ], + ), + const SizedBox(height: 12), + + // 网络数据信息 - 优化对齐方式 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: _buildNetworkDataItemAligned( + '上传', + '${player.uploadSpeed} KB/s', + Icons.upload, + colorScheme.primary, + ), + ), + Expanded( + child: _buildNetworkDataItemAligned( + '下载', + '${player.downloadSpeed} KB/s', + Icons.download, + colorScheme.secondary, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: _buildNetworkDataItemAligned( + '发送包', + '${player.sentPackets}', + Icons.send, + colorScheme.primary, + ), + ), + Expanded( + child: _buildNetworkDataItemAligned( + '接收包', + '${player.receivedPackets}', + Icons.call_received, + colorScheme.secondary, + ), + ), + ], + ), + ], + ), + ), + + const Divider(height: 24), + + // 丢包率信息 + _buildInfoRow( + Icons.error_outline, + '丢包率', + '${player.packetLossRate}%', + colorScheme, + valueColor: _getPacketLossColor(player.packetLossRate), + ), + ], + ), + ); + } + +// 构建列表项视图 + Widget _buildPlayerListItem(PlayerInfo player, ColorScheme colorScheme) { + // 根据延迟值确定颜色 + Color latencyColor = _getLatencyColor(player.latency); + // 根据连接类型选择图标 + IconData connectionIcon = _getConnectionIcon(player.connectionType); + + return FloatingCard( + colorScheme: colorScheme, + maxWidth: double.infinity, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 左侧玩家信息 + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 玩家名称和连接类型 + Row( + children: [ + Icon(Icons.person, color: colorScheme.primary, size: 22), + const SizedBox(width: 8), + Expanded( + child: Text( + player.name, + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 8), + + // IP地址 + _buildInfoRow( + Icons.lan, + 'IP地址', + player.ip, + colorScheme, + showCopyButton: true, + ), + const SizedBox(height: 8), + + // ET版本 + _buildInfoRow( + Icons.memory, + 'ET版本', + player.etVersion, + colorScheme, + ), + ], + ), + ), + + // 右侧网络状态信息 + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 连接类型标签 + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _getConnectionTypeColor( + player.connectionType, colorScheme), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + connectionIcon, + size: 14, + color: Colors.white, + ), + const SizedBox(width: 4), + Text( + player.connectionType, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + + // 延迟信息 + _buildInfoRow( + Icons.speed, + '延迟', + '${player.latency} ms', + colorScheme, + valueColor: latencyColor, + ), + const SizedBox(height: 8), + + // 丢包率信息 + _buildInfoRow( + Icons.error_outline, + '丢包率', + '${player.packetLossRate}%', + colorScheme, + valueColor: _getPacketLossColor(player.packetLossRate), + ), + ], + ), + ), + ], + ), + ); + } + + // 构建信息行 + // 构建信息行 + Widget _buildInfoRow( + IconData icon, + String label, + String value, + ColorScheme colorScheme, { + Color? valueColor, + bool showCopyButton = false, + }) { + return Row( + children: [ + Icon(icon, size: 20, color: colorScheme.primary), + const SizedBox(width: 12), + Text( + '$label:', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(width: 8), + // 添加复制按钮到标签和值之间 + if (showCopyButton) + IconButton( + icon: const Icon(Icons.copy, size: 18), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + tooltip: '复制$label', + onPressed: () { + // 复制到剪贴板 + Clipboard.setData(ClipboardData(text: value)); + // 显示提示 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('已复制: $value'), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + ), + ); + }, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + value, + style: TextStyle( + color: valueColor ?? colorScheme.secondary, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } + + // 构建对齐的网络数据项 + Widget _buildNetworkDataItemAligned( + String label, + String value, + IconData icon, + Color color, + ) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(icon, size: 22, color: color), + const SizedBox(height: 6), + Text( + label, + style: const TextStyle(fontSize: 13), + textAlign: TextAlign.center, + ), + const SizedBox(height: 2), + Text( + value, + style: TextStyle( + fontWeight: FontWeight.bold, + color: color, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ); + } + + // 根据延迟值获取颜色 + Color _getLatencyColor(int latency) { + if (latency < 50) { + return Colors.green; + } else if (latency < 100) { + return Colors.orange; + } else { + return Colors.red; + } + } + + // 根据丢包率获取颜色 + Color _getPacketLossColor(double lossRate) { + if (lossRate < 1.0) { + return Colors.green; + } else if (lossRate < 5.0) { + return Colors.orange; + } else { + return Colors.red; + } + } + + // 如果传入数值=1就是p2p 否则是relay 最后判断是不是等于本机IP如果等于就是direct 本机ip传入 + String _mapConnectionType(int connType, String ip, String thisip) { + // 新增服务器IP判断 + if (ip == "0.0.0.0") { + return '服务器'; + } + // 如果是本机IP,返回direct + if (ip == thisip) { + return '本机'; + } + // 根据连接成本判断连接类型 + if (connType == 1) { + return '直链'; + } else if (connType >= 2) { + return '中转'; + } + return '未知'; + } + + // 根据连接类型获取图标 + IconData _getConnectionIcon(String connectionType) { + // 将连接类型转为小写并进行匹配 + String lowerType = connectionType.toLowerCase(); + // 新增服务器图标 + if (lowerType.contains('server') || lowerType.contains('服务器')) { + return Icons.dns; + } else if (lowerType.contains('p2p') || lowerType.contains('直链')) { + return Icons.link; + } else if (lowerType.contains('relay') || lowerType.contains('中转')) { + return Icons.swap_horiz; + } else if (lowerType.contains('direct') || lowerType.contains('本机')) { + return Icons.computer; + } else { + return Icons.device_unknown; + } + } + + // 根据连接类型获取颜色 + Color _getConnectionTypeColor( + String connectionType, ColorScheme colorScheme) { + // 将连接类型转为小写并进行匹配 + String lowerType = connectionType.toLowerCase(); + if (lowerType.contains('server') || lowerType.contains('服务器')) { + return Colors.deepPurple; + } else if (lowerType.contains('p2p') || lowerType.contains('直链')) { + return Colors.green; + } else if (lowerType.contains('relay') || lowerType.contains('中转')) { + return Colors.orange; + } else if (lowerType.contains('direct') || lowerType.contains('本机')) { + return colorScheme.primary; + } else { + return Colors.grey; + } + } +} diff --git a/lib/screens/设置.dart b/lib/screens/设置.dart new file mode 100644 index 0000000..e78a284 --- /dev/null +++ b/lib/screens/设置.dart @@ -0,0 +1,415 @@ +import 'package:flutter/material.dart'; +import '../config/app_config.dart'; +import '../utils/ping_util.dart'; +import 'package:ASTRAL/utils/状态.dart'; +import 'package:provider/provider.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + bool _notificationsEnabled = true; + double _fontSize = 16.0; + late List _serverList; + late String _currentServer; + final _appConfig = AppConfig(); + bool _closeToTray = false; // 添加关闭进入托盘变量 + + String serverIP = ""; + // 添加 ping 相关状态 + Map pingResults = {}; + Map isPinging = {}; + + @override + void initState() { + super.initState(); + _serverList = _appConfig.serverList; + _currentServer = _appConfig.currentServer; + serverIP = _appConfig.currentServer; + _closeToTray = _appConfig.closeToTray; // 初始化托盘设置 + + // 初始化 ping 状态 + for (var server in _serverList) { + pingResults[server] = null; + isPinging[server] = false; + } + + // 开始 ping 当前服务器,并设置为持续 ping + _startPingServer(_currentServer, forceContinuous: true); + } + + @override + void dispose() { + // 停止所有 ping + for (var server in _serverList) { + _stopPingServer(server); + } + super.dispose(); + } + + // 修改开始 ping 方法,添加强制持续 ping 参数 + void _startPingServer(String server, {bool forceContinuous = false}) { + if (isPinging[server] == true) return; + + isPinging[server] = true; + if (forceContinuous) { + isPinging[server] = true; // 设置为持续 ping 状态 + } + _pingServer(server); + } + + // 修改停止 ping 方法 + void _stopPingServer(String server) { + // 如果是当前服务器,不允许停止 + if (server == _currentServer) return; + isPinging[server] = false; + } + + // 执行 ping 操作 + Future _pingServer(String server) async { + if (isPinging[server] != true) return; + + final pingResult = await PingUtil.ping(server); + + if (mounted) { + setState(() { + pingResults[server] = pingResult; + }); + + // 1秒后再次 ping + Future.delayed(const Duration(seconds: 1), () { + _pingServer(server); + }); + } + } + + // 添加服务器对话框 + Future _showAddServerDialog() async { + final controller = TextEditingController(); + final result = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('添加服务器'), + content: TextField( + controller: controller, + decoration: const InputDecoration( + labelText: '服务器地址', + hintText: 'example.com:port', + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + TextButton( + onPressed: () => Navigator.pop(context, controller.text), + child: const Text('添加'), + ), + ], + ), + ); + + if (result != null && result.isNotEmpty) { + setState(() { + _serverList.add(result); + _appConfig.setServerList(_serverList); + + // 初始化新服务器的 ping 状态 + pingResults[result] = null; + isPinging[result] = false; + }); + } + } + + // 编辑服务器对话框 + Future _showEditServerDialog(int index) async { + final controller = TextEditingController(text: _serverList[index]); + final result = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('编辑服务器'), + content: TextField( + controller: controller, + decoration: const InputDecoration( + labelText: '服务器地址', + hintText: 'example.com:port', + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + TextButton( + onPressed: () => Navigator.pop(context, controller.text), + child: const Text('保存'), + ), + ], + ), + ); + + if (result != null && result.isNotEmpty) { + setState(() { + _serverList[index] = result; + _appConfig.setServerList(_serverList); + }); + } + } + + // 删除服务器 + Future _deleteServer(int index) async { + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('确认删除'), + content: const Text('确定要删除这个服务器吗?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('取消'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('删除'), + ), + ], + ), + ); + + if (confirm == true) { + final server = _serverList[index]; + + // 停止 ping + _stopPingServer(server); + + setState(() { + _serverList.removeAt(index); + _appConfig.setServerList(_serverList); + + // 移除 ping 状态 + pingResults.remove(server); + isPinging.remove(server); + + if (_currentServer == server && _serverList.isNotEmpty) { + _currentServer = _serverList[0]; + _appConfig.setCurrentServer(_currentServer); + _startPingServer(_currentServer); + } + }); + } + } + + // 添加构建 ping 显示组件的方法 + Widget _buildPingWidget(String server) { + final pingResult = pingResults[server]; + if (server == _currentServer || isPinging[server] == true) { + if (pingResult == null) { + return const Text('测试中...', style: TextStyle(color: Colors.grey)); + } else { + return Text( + '${pingResult}ms', + style: TextStyle( + color: pingResult < 100 + ? Colors.green + : (pingResult < 300 ? Colors.orange : Colors.red), + ), + ); + } + } + return const Text('点击测试', style: TextStyle(color: Colors.grey)); + } + + @override + Widget build(BuildContext context) { + serverIP = Provider.of(context).virtualIP; + return ListView( + padding: const EdgeInsets.all(16.0), + children: [ + Card( + child: Column( + children: [ + ListTile( + leading: const Icon(Icons.dns), + title: Row( + children: [ + const Text('当前服务器'), + const SizedBox(width: 8), + _buildPingWidget(_currentServer), + ], + ), + subtitle: Text(_currentServer), + ), + ExpansionTile( + leading: const Icon(Icons.list), + title: const Text('服务器列表'), + onExpansionChanged: (expanded) { + // 展开/折叠时处理其他服务器的 ping 状态 + setState(() { + for (var server in _serverList) { + if (server != _currentServer) { + if (expanded) { + // 如果之前是手动开启的,则恢复 ping + if (isPinging[server] == true) { + _startPingServer(server); + } + } else { + // 折叠时暂停所有非当前服务器的 ping + _stopPingServer(server); + } + } + } + }); + }, + children: [ + // 在服务器列表前添加当前服务器的 ping 状态显示 + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _serverList.length, + itemBuilder: (context, index) { + final server = _serverList[index]; + final pingResult = pingResults[server]; + + // 构建延迟显示组件 + Widget pingWidget; + if (isPinging[server] == true) { + if (pingResult == null) { + pingWidget = const Text('测试中...', + style: TextStyle(color: Colors.grey)); + } else { + pingWidget = Text('${pingResult}ms', + style: TextStyle( + color: pingResult < 100 + ? Colors.green + : (pingResult < 300 + ? Colors.orange + : Colors.red))); + } + } else { + pingWidget = const Text('点击测试', + style: TextStyle(color: Colors.grey)); + } + + return ListTile( + leading: const Icon(Icons.computer), + title: Row( + children: [ + Text('服务器 ${index + 1}'), + const SizedBox(width: 8), + pingWidget, + ], + ), + subtitle: Text(server), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // 添加 ping 按钮 + IconButton( + icon: Icon( + isPinging[server] == true + ? Icons.pause + : Icons.play_arrow, + color: isPinging[server] == true + ? Colors.blue + : null, + ), + onPressed: () { + setState(() { + if (isPinging[server] == true) { + _stopPingServer(server); + } else { + _startPingServer(server); + } + }); + }, + ), + IconButton( + icon: const Icon(Icons.edit), + onPressed: () => _showEditServerDialog(index), + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => _deleteServer(index), + ), + ], + ), + onTap: () { + setState(() { + _currentServer = server; + Provider.of(context, listen: false).serverIP = + server; + // 开始 ping 新选择的服务器 + _startPingServer(server); + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('已切换到服务器: $server')), + ); + }, + ); + }, + ), + ListTile( + leading: const Icon(Icons.add), + title: const Text('添加新服务器'), + onTap: _showAddServerDialog, + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + + // 添加应用设置卡片 + Card( + child: Column( + children: [ + const ListTile( + leading: Icon(Icons.settings), + title: Text('应用设置'), + ), + SwitchListTile( + title: const Text('关闭时最小化到托盘'), + subtitle: const Text('关闭窗口时应用将继续在后台运行'), + value: _closeToTray, + onChanged: (value) { + setState(() { + _closeToTray = value; + _appConfig.setCloseToTray(value); + }); + }, + ), + ], + ), + ), + + const SizedBox(height: 16), + Card( + child: Column( + children: [ + const ListTile( + leading: Icon(Icons.info), + title: Text('应用版本'), + subtitle: Text('灰度版本'), + ), + ListTile( + leading: const Icon(Icons.update), + title: const Text('检查更新'), + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('灰度版本不支持更新')), + ); + }, + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/screens/首页.dart b/lib/screens/首页.dart new file mode 100644 index 0000000..e2902ff --- /dev/null +++ b/lib/screens/首页.dart @@ -0,0 +1,922 @@ +// 导入必要的包 +import 'dart:convert'; + +import 'package:ASTRAL/src/rust/api/simple.dart'; +import 'package:ASTRAL/utils/%E7%8A%B6%E6%80%81.dart'; +import 'package:ASTRAL/utils/app_info.dart'; +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'dart:async'; +import 'package:provider/provider.dart'; +import '../widgets/卡片.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import '../utils/runin.dart'; + +enum ConnectionState { notStarted, connecting, connected } + +Runin parseRunin(String jsonString) { + final Map jsonMap = json.decode(jsonString); + return Runin.fromJson(jsonMap); +} + +// 辅助方法:将整数形式的IP转换为字符串 +String _intToIpv4String(int addr) { + return [ + (addr >> 24) & 0xFF, + (addr >> 16) & 0xFF, + (addr >> 8) & 0xFF, + addr & 0xFF, + ].join('.'); +} + +/// 首页组件 +/// 用于显示应用的主页面,包含主题切换和问候功能 +class HomePage extends StatefulWidget { + // 主题模式切换回调函数 + final Function toggleThemeMode; + // 主题色更改回调函数 + final Function(Color) changeSeedColor; + // 当前主题模式 + final ThemeMode currentThemeMode; + + /// 构造函数 + const HomePage({ + super.key, + required this.toggleThemeMode, + required this.changeSeedColor, + required this.currentThemeMode, + }); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + // 定义状态枚举 + + // 当前连接状态 + ConnectionState _connectionState = ConnectionState.notStarted; + + bool isRunning = false; // 只用于控制启动/暂停状态 + Duration runningTime = Duration.zero; + Timer? timer; + + // 模拟数据 + double uploadSpeed = 0; // MB/s + double downloadSpeed = 0; // MB/s + String publicIP = ""; + // 房间名 + String roomName = ""; + // 房间密码 + String roomPassword = ""; + // 用户名 + String username = ""; + + bool _isAutoIP = true; // 只用于控制IP自动/手动模式 + + String Serverip = ""; + int _uploadBytes = 0; + int _downloadBytes = 0; + int _lastUploadBytes = 0; + int _lastDownloadBytes = 0; + // 定义卡片列表 + late final List _cardBuilders; + + // 添加 TextEditingController + late final TextEditingController _roomNameController; + late final TextEditingController _roomPasswordController; + late final TextEditingController _usernameController; + late final TextEditingController _virtualIPController; + + @override + void initState() { + super.initState(); + // 初始化 TextEditingController + _roomNameController = TextEditingController(text: roomName); + _roomPasswordController = TextEditingController(text: roomPassword); + _usernameController = TextEditingController(text: username); + _virtualIPController = TextEditingController(text: publicIP); + + // 修改卡片构建器列表,添加版本信息卡片 + _cardBuilders = [ + _buildNetworkStatusCard, // 网络状态卡片 + _buildUserInfoCard, // 用户信息卡片 + _buildRoomInfoCard, // 房间信息卡片 + _buildVersionInfoCard, // 新增版本信息卡片 + ]; + } + + void toggleRunning() { + final km = Provider.of(context, listen: false); + setState(() { + isRunning = !isRunning; + if (isRunning) { + // 切换到连接中状态 + _connectionState = ConnectionState.connecting; + createServer( + username: username, + enableDhcp: _isAutoIP, + specifiedIp: publicIP, + roomName: roomName, + roomPassword: roomPassword, + severurl: Serverip); + // 模拟连接过程,2秒后连接成功 + Future.delayed(const Duration(seconds: 2), () { + if (isRunning) { + // 确保用户没有在连接过程中取消 + setState(() { + _connectionState = ConnectionState.connected; + // 连接成功后开始计时 + + timer = Timer.periodic(const Duration(seconds: 1), (timer) async { + final info = await getRunningInfo(); + // 打印运行信息的详细内容 + // print("运行信息详情:${info}"); + Runin runin = parseRunin(info); + // 获取网络状态 + // 获取网络状态 + final networkStatus = await getNetworkStatus(); + km.nodes = networkStatus.nodes; + + // 更新网络流量数据 + _updateNetworkStats(networkStatus.nodes); + + // print('设备名称: ${runin.devName}'); + // print( + // '设备ID: ${_intToIpv4String(runin.myNodeInfo?.virtualIpv4?.address?.addr ?? 0)}'); + final int? version = + runin.myNodeInfo?.virtualIpv4?.address?.addr; + if (version != null) { + String ipStr = _intToIpv4String(version); + // 检查IP不为0.0.0.0且与当前IP不同时才更新 + if (ipStr != "0.0.0.0" && publicIP != ipStr) { + km.virtualIP = ipStr; + } + } + // print("- 用户名: ${info?.myNodeInfo?.hostname}"); + // print("- 虚拟IPv4: ${info?.myNodeInfo?.virtualIpv4?.address}"); + // print("- version: ${info?.myNodeInfo?.version}"); + // print("- 本地IP: ${info.myNodeInfo.}"); + setState(() { + runningTime += const Duration(seconds: 1); + }); + }); + }); + } + }); + } else { + // 停止时重置状态 + _connectionState = ConnectionState.notStarted; + closeAllServer(); + timer?.cancel(); + runningTime = Duration.zero; + // 重置网络统计数据 + _uploadBytes = 0; + _downloadBytes = 0; + _lastUploadBytes = 0; + _lastDownloadBytes = 0; + uploadSpeed = 0; + downloadSpeed = 0; + } + }); + } + + // 添加网络统计数据更新方法 + void _updateNetworkStats(dynamic nodes) { + if (nodes == null || nodes.isEmpty) return; + + int totalUploadBytes = 0; + int totalDownloadBytes = 0; + String myIP = Provider.of(context, listen: false).virtualIP; + + // 查找本机节点 + for (var node in nodes) { + if (node.ipv4 == myIP) { + // 找到本机节点,计算上传下载总量 + if (node.connections != null && node.connections.isNotEmpty) { + for (var conn in node.connections) { + totalUploadBytes += (conn.txBytes as BigInt).toInt(); + totalDownloadBytes += (conn.rxBytes as BigInt).toInt(); + } + } + break; + } + } + + // 计算速度 (字节/秒 转换为 MB/秒) + setState(() { + _uploadBytes = totalUploadBytes; + _downloadBytes = totalDownloadBytes; + + // 计算速度差值 + if (_lastUploadBytes > 0) { + uploadSpeed = + (_uploadBytes - _lastUploadBytes) / (1024 * 1024); // 转换为MB/s + uploadSpeed = double.parse(uploadSpeed.toStringAsFixed(2)); // 保留两位小数 + } + + if (_lastDownloadBytes > 0) { + downloadSpeed = + (_downloadBytes - _lastDownloadBytes) / (1024 * 1024); // 转换为MB/s + downloadSpeed = + double.parse(downloadSpeed.toStringAsFixed(2)); // 保留两位小数 + } + + // 更新上次的值 + _lastUploadBytes = _uploadBytes; + _lastDownloadBytes = _downloadBytes; + }); + } + + // 根据屏幕宽度计算列数 + int _getColumnCount(BuildContext context) { + final width = MediaQuery.of(context).size.width; + if (width < 600) { + return 1; // 手机屏幕显示1列 + } else if (width < 900) { + return 2; // 平板或小屏幕显示2列 + } else { + return 3; // 大屏幕显示3列 + } + } + + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + String hours = twoDigits(duration.inHours); + String minutes = twoDigits(duration.inMinutes.remainder(60)); + String seconds = twoDigits(duration.inSeconds.remainder(60)); + return '$hours:$minutes:$seconds'; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final km = Provider.of(context); + _roomNameController.value = TextEditingValue( + text: km.roomName, + selection: _roomNameController.selection, + ); + _roomPasswordController.value = TextEditingValue( + text: km.roomPassword, + selection: _roomPasswordController.selection, + ); + _usernameController.value = TextEditingValue( + text: km.username, + selection: _usernameController.selection, + ); + _virtualIPController.value = TextEditingValue( + text: km.virtualIP, + selection: _virtualIPController.selection, + ); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + publicIP = Provider.of(context).virtualIP; + _isAutoIP = Provider.of(context).dynamicIP; // 更新自动IP状态 + //我的房间 + roomName = Provider.of(context).roomName; + //我的密码 + roomPassword = Provider.of(context).roomPassword; + //我的用户名 + username = Provider.of(context).username; + Serverip = Provider.of(context).serverIP; + // 使用 SliverPadding 包裹 SliverList + return Scaffold( + body: CustomScrollView( + // 添加这个属性来控制滚动行为 + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + // 替换原有的 SliverList 为 SliverPadding + SliverGrid + SliverPadding( + padding: const EdgeInsets.all(16.0), + sliver: SliverMasonryGrid.count( + crossAxisCount: _getColumnCount(context), // 根据屏幕宽度动态设置列数 + mainAxisSpacing: 16, // 主轴间距 + crossAxisSpacing: 16, // 交叉轴间距 + childCount: _cardBuilders.length, + itemBuilder: (context, index) { + // 直接从列表中获取构建函数并调用 + return ConstrainedBox( + constraints: const BoxConstraints(minHeight: 100), + child: _cardBuilders[index](colorScheme), + ); + }, + ), + ), + ], + ), + floatingActionButton: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + width: _connectionState != ConnectionState.notStarted ? 180 : 100, + height: 60, + child: FloatingActionButton.extended( + onPressed: _connectionState == ConnectionState.connecting + ? null + : toggleRunning, + extendedPadding: const EdgeInsets.symmetric(horizontal: 2), + splashColor: _connectionState != ConnectionState.notStarted + ? colorScheme.onTertiary.withOpacity(0.2) + : colorScheme.onPrimary.withOpacity(0.2), + highlightElevation: 6, + elevation: 2, + icon: AnimatedSwitcher( + duration: const Duration(milliseconds: 150), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: child, + ), + ); + }, + child: _getButtonIcon(_connectionState), + ), + label: AnimatedSwitcher( + duration: const Duration(milliseconds: 0), + switchInCurve: Curves.easeOutQuad, + switchOutCurve: Curves.easeInQuad, + child: _getButtonLabel(_connectionState), + ), + backgroundColor: _getButtonColor(_connectionState, colorScheme), + foregroundColor: + _getButtonForegroundColor(_connectionState, colorScheme), + ), + ), + ); + } + +// 根据连接状态获取按钮图标 + Widget _getButtonIcon(ConnectionState state) { + switch (state) { + case ConnectionState.notStarted: + return const Icon( + Icons.play_arrow, + key: ValueKey('play'), + size: 34, + ); + case ConnectionState.connecting: + return SizedBox( + key: const ValueKey('connecting'), + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2.5, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.onTertiary), + ), + ); + case ConnectionState.connected: + return const Icon( + Icons.pause, + key: ValueKey('pause'), + size: 34, + ); + } + } + + // 根据连接状态获取按钮文本 + Widget _getButtonLabel(ConnectionState state) { + switch (state) { + case ConnectionState.notStarted: + return const Text( + '启动', + key: ValueKey('start_text'), + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ); + case ConnectionState.connecting: + return const Text( + '连接中...', + key: ValueKey('connecting_text'), + style: TextStyle(fontWeight: FontWeight.bold), + ); + case ConnectionState.connected: + return Text( + _formatDuration(runningTime), + key: const ValueKey('running_text'), + style: const TextStyle(fontWeight: FontWeight.bold), + ); + } + } + + // 根据连接状态获取按钮背景色 + Color _getButtonColor(ConnectionState state, ColorScheme colorScheme) { + switch (state) { + case ConnectionState.notStarted: + return colorScheme.primary; + case ConnectionState.connecting: + return colorScheme.tertiary.withOpacity(0.7); + case ConnectionState.connected: + return colorScheme.tertiary; + } + } + + // 根据连接状态获取按钮前景色 + Color _getButtonForegroundColor( + ConnectionState state, ColorScheme colorScheme) { + switch (state) { + case ConnectionState.notStarted: + return colorScheme.onPrimary; + case ConnectionState.connecting: + case ConnectionState.connected: + return colorScheme.onTertiary; + } + } + + // 修改卡片构建方法,移除多余的内边距 + Widget _buildDashboardCard(ColorScheme colorScheme) { + return FloatingCard( + colorScheme: colorScheme, + maxWidth: 600, // 设置最大宽度 + height: 200, + child: SizedBox( + child: PieChart( + PieChartData( + sections: [ + PieChartSectionData( + value: uploadSpeed, + title: '上传', + color: colorScheme.primary, + ), + PieChartSectionData( + value: downloadSpeed, + title: '下载', + color: colorScheme.secondary, + ), + ], + ), + ), + )); + } + + // 修改流量统计卡片,移除多余的内边距 + Widget _buildTrafficCard(ColorScheme colorScheme) { + return FloatingCard( + colorScheme: colorScheme, + maxWidth: 600, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 修改标题为图标+文字组合 + Row( + children: [ + Icon(Icons.data_usage, color: colorScheme.primary, size: 22), + const SizedBox(width: 8), + const Text('流量统计', + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildTrafficInfo('上传速度', '$uploadSpeed MB/s', Icons.upload, + colorScheme.primary), + _buildTrafficInfo('下载速度', '$downloadSpeed MB/s', Icons.download, + colorScheme.secondary), + ], + ), + ], + )); + } + + // 修改IP地址卡片,移除多余的内边距 + Widget _buildIPCard(ColorScheme colorScheme) { + return FloatingCard( + colorScheme: colorScheme, + maxWidth: 600, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 修改标题为图标+文字组合 + Row( + children: [ + Icon(Icons.wifi, color: colorScheme.primary, size: 22), + const SizedBox(width: 8), + const Text('网络信息', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 16), + _buildIPInfo('虚拟 IP', publicIP, Icons.public, colorScheme), + ], + ), + ); + } + + Widget _buildTrafficInfo( + String label, String value, IconData icon, Color color) { + return Column( + children: [ + Icon(icon, size: 24, color: color), + const SizedBox(height: 8), + Text(label), + Text(value, + style: TextStyle(fontWeight: FontWeight.bold, color: color)), + ], + ); + } + + Widget _buildIPInfo( + String label, String value, IconData icon, ColorScheme colorScheme) { + return Row( + children: [ + Icon(icon, size: 20, color: colorScheme.primary), + const SizedBox(width: 8), + Text('$label: ', style: const TextStyle(fontWeight: FontWeight.bold)), + Text(value, style: TextStyle(color: colorScheme.secondary)), + ], + ); + } + + // 新增合并后的网络状态卡片(合并了流量统计和IP信息) + Widget _buildNetworkStatusCard(ColorScheme colorScheme) { + return FloatingCard( + colorScheme: colorScheme, + maxWidth: 600, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题栏带有连接状态指示器 + Row( + children: [ + Icon(Icons.network_check, color: colorScheme.primary, size: 22), + const SizedBox(width: 8), + const Text('网络状态', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Spacer(), + // 添加状态指示器 + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _getStatusColor(_connectionState, colorScheme), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + _getStatusText(_connectionState), + style: TextStyle( + color: colorScheme.onPrimary, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + // IP信息部分 + _buildIPInfo('虚拟 IP', publicIP, Icons.public, colorScheme), + const SizedBox(height: 12), + + // 流量统计部分 + const Divider(), + const SizedBox(height: 8), + Row( + children: [ + Icon(Icons.data_usage, color: colorScheme.primary, size: 18), + const SizedBox(width: 8), + Text('流量统计', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: colorScheme.primary, + )), + ], + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildTrafficInfo('上传速度', '$uploadSpeed MB/s', Icons.upload, + colorScheme.primary), + _buildTrafficInfo('下载速度', '$downloadSpeed MB/s', Icons.download, + colorScheme.secondary), + ], + ), + + // 添加运行时间显示 + if (_connectionState == ConnectionState.connected) ...[ + const Divider(), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.timer, size: 16, color: colorScheme.primary), + const SizedBox(width: 8), + Text( + '运行时间: ${_formatDuration(runningTime)}', + style: TextStyle( + fontWeight: FontWeight.w500, + color: colorScheme.primary, + ), + ), + ], + ), + ], + ], + ), + ); + } + + // 获取状态文本 + String _getStatusText(ConnectionState state) { + switch (state) { + case ConnectionState.notStarted: + return '未连接'; + case ConnectionState.connecting: + return '连接中'; + case ConnectionState.connected: + return '已连接'; + } + } + + // 获取状态颜色 + Color _getStatusColor(ConnectionState state, ColorScheme colorScheme) { + switch (state) { + case ConnectionState.notStarted: + return Colors.grey; + case ConnectionState.connecting: + return Colors.orange; + case ConnectionState.connected: + return Colors.green; + } + } + + // 优化用户信息卡片 + Widget _buildUserInfoCard(ColorScheme colorScheme) { + final km = Provider.of(context, listen: false); + final isValidIP = _isAutoIP || _isValidIPv4(km.virtualIP); + + return FloatingCard( + colorScheme: colorScheme, + maxWidth: 600, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题栏 + Row( + children: [ + Icon(Icons.person, color: colorScheme.primary, size: 22), + const SizedBox(width: 8), + const Text('用户信息', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Spacer(), + // 添加编辑状态指示器 + if (_connectionState == ConnectionState.connected) + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + '已锁定', + style: TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + // 用户名输入框 + TextField( + controller: _usernameController, + enabled: _connectionState != ConnectionState.connected, + onChanged: (value) { + km.username = value; + }, + decoration: InputDecoration( + labelText: '用户名', + border: const OutlineInputBorder(), + prefixIcon: Icon(Icons.person, color: colorScheme.primary), + floatingLabelBehavior: FloatingLabelBehavior.always, + ), + ), + const SizedBox(height: 12), + + // IP设置部分 + Row( + children: [ + Expanded( + child: TextField( + controller: _virtualIPController, + enabled: !_isAutoIP && + _connectionState != ConnectionState.connected, + onChanged: (value) { + if (!_isAutoIP) { + setState(() {}); + km.virtualIP = value; + } + }, + decoration: InputDecoration( + labelText: '虚拟网IP', + border: const OutlineInputBorder(), + prefixIcon: Icon(Icons.lan, color: colorScheme.primary), + floatingLabelBehavior: FloatingLabelBehavior.always, + errorText: !isValidIP && !_isAutoIP ? '请输入有效的IPv4地址' : null, + ), + ), + ), + const SizedBox(width: 8), + Column( + children: [ + Switch( + value: _isAutoIP, + onChanged: _connectionState != ConnectionState.connected + ? (value) { + setState(() { + _isAutoIP = value; + }); + km.dynamicIP = value; + } + : null, + ), + Text( + _isAutoIP ? "自动" : "手动", + style: const TextStyle(fontSize: 12), + ), + ], + ), + ], + ), + if (_isAutoIP) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '系统将自动分配虚拟网IP', + style: TextStyle( + color: colorScheme.secondary, + fontSize: 12, + ), + ), + ), + ], + ), + ); + } + + // 优化房间信息卡片 + Widget _buildRoomInfoCard(ColorScheme colorScheme) { + final km = Provider.of(context, listen: false); + return FloatingCard( + colorScheme: colorScheme, + maxWidth: 600, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题栏 + Row( + children: [ + Icon(Icons.meeting_room, color: colorScheme.primary, size: 22), + const SizedBox(width: 8), + const Text('房间信息', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Spacer(), + // 添加编辑状态指示器 + if (_connectionState == ConnectionState.connected) + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + '已锁定', + style: TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + // 房间名称输入框 + TextField( + controller: _roomNameController, + enabled: _connectionState != ConnectionState.connected, + onChanged: (value) { + km.roomName = value; + }, + decoration: InputDecoration( + labelText: '房间名称', + border: const OutlineInputBorder(), + prefixIcon: Icon(Icons.meeting_room, color: colorScheme.primary), + floatingLabelBehavior: FloatingLabelBehavior.always, + ), + ), + const SizedBox(height: 12), + + // 房间密码输入框 + TextField( + controller: _roomPasswordController, + enabled: _connectionState != ConnectionState.connected, + onChanged: (value) { + km.roomPassword = value; + }, + obscureText: true, + decoration: InputDecoration( + labelText: '房间密码', + border: const OutlineInputBorder(), + prefixIcon: Icon(Icons.lock, color: colorScheme.primary), + floatingLabelBehavior: FloatingLabelBehavior.always, + // helperText: '留空表示无密码', + ), + ), + ], + ), + ); + } + + // 添加IPv4地址验证方法 + bool _isValidIPv4(String ip) { + if (ip.isEmpty) return false; + + // 使用正则表达式验证IPv4地址格式 + final ipv4Pattern = RegExp( + r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'); + + return ipv4Pattern.hasMatch(ip); + } +} + +// 添加版本信息卡片 +Widget _buildVersionInfoCard(ColorScheme colorScheme) { + // 这里可以从配置或API获取实际版本号 + final String appVersion = AppInfoUtil.getFullVersion(); + + return FloatingCard( + colorScheme: colorScheme, + maxWidth: 600, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题栏 + Row( + children: [ + Icon(Icons.info_outline, color: colorScheme.primary, size: 22), + const SizedBox(width: 8), + const Text('版本信息', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 16), + + // 版本信息列表 + _buildVersionItem('软件版本', appVersion, Icons.apps, colorScheme), + const SizedBox(height: 12), + + // 使用FutureBuilder处理异步获取的版本信息 + FutureBuilder( + future: easytierVersion(), + builder: (context, snapshot) { + String version = snapshot.hasData ? snapshot.data! : "加载中..."; + return _buildVersionItem( + 'ET内核版本', version, Icons.memory, colorScheme); + }, + ), + ], + ), + ); +} + +// 版本信息项构建方法 +Widget _buildVersionItem( + String label, String version, IconData icon, ColorScheme colorScheme) { + return Row( + children: [ + Icon(icon, size: 20, color: colorScheme.primary), + const SizedBox(width: 12), + Text('$label:', + style: const TextStyle( + fontWeight: FontWeight.bold, + )), + const SizedBox(width: 8), + Text( + version, + style: TextStyle( + color: colorScheme.secondary, + fontWeight: FontWeight.w500, + ), + ), + ], + ); +} diff --git a/lib/src/rust/api/simple.dart b/lib/src/rust/api/simple.dart new file mode 100644 index 0000000..9d03672 --- /dev/null +++ b/lib/src/rust/api/simple.dart @@ -0,0 +1,159 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +// These functions are ignored because they are not marked as `pub`: `create_and_store_network_instance`, `create_config` +// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `RT` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `deref`, `initialize` + +Future<(List, List)> getPeersAndRoutes() => + RustLib.instance.api.crateApiSimpleGetPeersAndRoutes(); + +Future> getPeerRoutePairs() => + RustLib.instance.api.crateApiSimpleGetPeerRoutePairs(); + +Future getNodeInfo() => + RustLib.instance.api.crateApiSimpleGetNodeInfo(); + +Future easytierVersion() => + RustLib.instance.api.crateApiSimpleEasytierVersion(); + +Future isEasytierRunning() => + RustLib.instance.api.crateApiSimpleIsEasytierRunning(); + +Future getNetworkStatus() => + RustLib.instance.api.crateApiSimpleGetNetworkStatus(); + +Future getRunningInfo() => + RustLib.instance.api.crateApiSimpleGetRunningInfo(); + +Future createServer( + {required String username, + required bool enableDhcp, + required String specifiedIp, + required String roomName, + required String roomPassword, + required String severurl}) => + RustLib.instance.api.crateApiSimpleCreateServer( + username: username, + enableDhcp: enableDhcp, + specifiedIp: specifiedIp, + roomName: roomName, + roomPassword: roomPassword, + severurl: severurl); + +Future closeAllServer() => + RustLib.instance.api.crateApiSimpleCloseAllServer(); + +Future initApp() => RustLib.instance.api.crateApiSimpleInitApp(); + +// Rust type: RustOpaqueMoi> +abstract class MyNodeInfo implements RustOpaqueInterface {} + +// Rust type: RustOpaqueMoi> +abstract class PeerInfo implements RustOpaqueInterface {} + +// Rust type: RustOpaqueMoi> +abstract class PeerRoutePair implements RustOpaqueInterface {} + +// Rust type: RustOpaqueMoi> +abstract class Route implements RustOpaqueInterface {} + +class KVNetworkStatus { + final BigInt totalNodes; + final List nodes; + + const KVNetworkStatus({ + required this.totalNodes, + required this.nodes, + }); + + @override + int get hashCode => totalNodes.hashCode ^ nodes.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is KVNetworkStatus && + runtimeType == other.runtimeType && + totalNodes == other.totalNodes && + nodes == other.nodes; +} + +class KVNodeConnectionStats { + final String connType; + final BigInt rxBytes; + final BigInt txBytes; + final BigInt rxPackets; + final BigInt txPackets; + + const KVNodeConnectionStats({ + required this.connType, + required this.rxBytes, + required this.txBytes, + required this.rxPackets, + required this.txPackets, + }); + + @override + int get hashCode => + connType.hashCode ^ + rxBytes.hashCode ^ + txBytes.hashCode ^ + rxPackets.hashCode ^ + txPackets.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is KVNodeConnectionStats && + runtimeType == other.runtimeType && + connType == other.connType && + rxBytes == other.rxBytes && + txBytes == other.txBytes && + rxPackets == other.rxPackets && + txPackets == other.txPackets; +} + +class KVNodeInfo { + final String hostname; + final String ipv4; + final double latencyMs; + final List connections; + final String version; + final int cost; + + const KVNodeInfo({ + required this.hostname, + required this.ipv4, + required this.latencyMs, + required this.connections, + required this.version, + required this.cost, + }); + + @override + int get hashCode => + hostname.hashCode ^ + ipv4.hashCode ^ + latencyMs.hashCode ^ + connections.hashCode ^ + version.hashCode ^ + cost.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is KVNodeInfo && + runtimeType == other.runtimeType && + hostname == other.hostname && + ipv4 == other.ipv4 && + latencyMs == other.latencyMs && + connections == other.connections && + version == other.version && + cost == other.cost; +} diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart new file mode 100644 index 0000000..c702b04 --- /dev/null +++ b/lib/src/rust/frb_generated.dart @@ -0,0 +1,1235 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +import 'api/simple.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'frb_generated.dart'; +import 'frb_generated.io.dart' + if (dart.library.js_interop) 'frb_generated.web.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +/// Main entrypoint of the Rust API +class RustLib extends BaseEntrypoint { + @internal + static final instance = RustLib._(); + + RustLib._(); + + /// Initialize flutter_rust_bridge + static Future init({ + RustLibApi? api, + BaseHandler? handler, + ExternalLibrary? externalLibrary, + }) async { + await instance.initImpl( + api: api, + handler: handler, + externalLibrary: externalLibrary, + ); + } + + /// Initialize flutter_rust_bridge in mock mode. + /// No libraries for FFI are loaded. + static void initMock({ + required RustLibApi api, + }) { + instance.initMockImpl( + api: api, + ); + } + + /// Dispose flutter_rust_bridge + /// + /// The call to this function is optional, since flutter_rust_bridge (and everything else) + /// is automatically disposed when the app stops. + static void dispose() => instance.disposeImpl(); + + @override + ApiImplConstructor get apiImplConstructor => + RustLibApiImpl.new; + + @override + WireConstructor get wireConstructor => + RustLibWire.fromExternalLibrary; + + @override + Future executeRustInitializers() async {} + + @override + ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig => + kDefaultExternalLibraryLoaderConfig; + + @override + String get codegenVersion => '2.9.0'; + + @override + int get rustContentHash => 486890655; + + static const kDefaultExternalLibraryLoaderConfig = + ExternalLibraryLoaderConfig( + stem: 'rust_lib_fltier', + ioDirectory: 'rust/target/release/', + webPrefix: 'pkg/', + ); +} + +abstract class RustLibApi extends BaseApi { + Future crateApiSimpleCloseAllServer(); + + Future crateApiSimpleCreateServer( + {required String username, + required bool enableDhcp, + required String specifiedIp, + required String roomName, + required String roomPassword, + required String severurl}); + + Future crateApiSimpleEasytierVersion(); + + Future crateApiSimpleGetNetworkStatus(); + + Future crateApiSimpleGetNodeInfo(); + + Future> crateApiSimpleGetPeerRoutePairs(); + + Future<(List, List)> crateApiSimpleGetPeersAndRoutes(); + + Future crateApiSimpleGetRunningInfo(); + + Future crateApiSimpleInitApp(); + + Future crateApiSimpleIsEasytierRunning(); + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_MyNodeInfo; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_MyNodeInfo; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_MyNodeInfoPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_PeerInfo; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_PeerInfo; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_PeerInfoPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_PeerRoutePair; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_PeerRoutePair; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_PeerRoutePairPtr; + + RustArcIncrementStrongCountFnType get rust_arc_increment_strong_count_Route; + + RustArcDecrementStrongCountFnType get rust_arc_decrement_strong_count_Route; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RoutePtr; +} + +class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { + RustLibApiImpl({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + @override + Future crateApiSimpleCloseAllServer() { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 1, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiSimpleCloseAllServerConstMeta, + argValues: [], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiSimpleCloseAllServerConstMeta => + const TaskConstMeta( + debugName: "close_all_server", + argNames: [], + ); + + @override + Future crateApiSimpleCreateServer( + {required String username, + required bool enableDhcp, + required String specifiedIp, + required String roomName, + required String roomPassword, + required String severurl}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(username, serializer); + sse_encode_bool(enableDhcp, serializer); + sse_encode_String(specifiedIp, serializer); + sse_encode_String(roomName, serializer); + sse_encode_String(roomPassword, serializer); + sse_encode_String(severurl, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 2, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiSimpleCreateServerConstMeta, + argValues: [ + username, + enableDhcp, + specifiedIp, + roomName, + roomPassword, + severurl + ], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiSimpleCreateServerConstMeta => const TaskConstMeta( + debugName: "create_server", + argNames: [ + "username", + "enableDhcp", + "specifiedIp", + "roomName", + "roomPassword", + "severurl" + ], + ); + + @override + Future crateApiSimpleEasytierVersion() { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 3, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiSimpleEasytierVersionConstMeta, + argValues: [], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiSimpleEasytierVersionConstMeta => + const TaskConstMeta( + debugName: "easytier_version", + argNames: [], + ); + + @override + Future crateApiSimpleGetNetworkStatus() { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 4, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_kv_network_status, + decodeErrorData: null, + ), + constMeta: kCrateApiSimpleGetNetworkStatusConstMeta, + argValues: [], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiSimpleGetNetworkStatusConstMeta => + const TaskConstMeta( + debugName: "get_network_status", + argNames: [], + ); + + @override + Future crateApiSimpleGetNodeInfo() { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 5, port: port_); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiSimpleGetNodeInfoConstMeta, + argValues: [], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiSimpleGetNodeInfoConstMeta => const TaskConstMeta( + debugName: "get_node_info", + argNames: [], + ); + + @override + Future> crateApiSimpleGetPeerRoutePairs() { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 6, port: port_); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiSimpleGetPeerRoutePairsConstMeta, + argValues: [], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiSimpleGetPeerRoutePairsConstMeta => + const TaskConstMeta( + debugName: "get_peer_route_pairs", + argNames: [], + ); + + @override + Future<(List, List)> crateApiSimpleGetPeersAndRoutes() { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 7, port: port_); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route, + decodeErrorData: sse_decode_String, + ), + constMeta: kCrateApiSimpleGetPeersAndRoutesConstMeta, + argValues: [], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiSimpleGetPeersAndRoutesConstMeta => + const TaskConstMeta( + debugName: "get_peers_and_routes", + argNames: [], + ); + + @override + Future crateApiSimpleGetRunningInfo() { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 8, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: null, + ), + constMeta: kCrateApiSimpleGetRunningInfoConstMeta, + argValues: [], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiSimpleGetRunningInfoConstMeta => + const TaskConstMeta( + debugName: "get_running_info", + argNames: [], + ); + + @override + Future crateApiSimpleInitApp() { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 9, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiSimpleInitAppConstMeta, + argValues: [], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiSimpleInitAppConstMeta => const TaskConstMeta( + debugName: "init_app", + argNames: [], + ); + + @override + Future crateApiSimpleIsEasytierRunning() { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 10, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bool, + decodeErrorData: null, + ), + constMeta: kCrateApiSimpleIsEasytierRunningConstMeta, + argValues: [], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiSimpleIsEasytierRunningConstMeta => + const TaskConstMeta( + debugName: "is_easytier_running", + argNames: [], + ); + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_MyNodeInfo => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_MyNodeInfo => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_PeerInfo => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_PeerInfo => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_PeerRoutePair => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_PeerRoutePair => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair; + + RustArcIncrementStrongCountFnType get rust_arc_increment_strong_count_Route => + wire.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute; + + RustArcDecrementStrongCountFnType get rust_arc_decrement_strong_count_Route => + wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute; + + @protected + MyNodeInfo + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return MyNodeInfoImpl.frbInternalDcoDecode(raw as List); + } + + @protected + PeerInfo + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return PeerInfoImpl.frbInternalDcoDecode(raw as List); + } + + @protected + PeerRoutePair + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return PeerRoutePairImpl.frbInternalDcoDecode(raw as List); + } + + @protected + Route + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return RouteImpl.frbInternalDcoDecode(raw as List); + } + + @protected + MyNodeInfo + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return MyNodeInfoImpl.frbInternalDcoDecode(raw as List); + } + + @protected + PeerInfo + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return PeerInfoImpl.frbInternalDcoDecode(raw as List); + } + + @protected + PeerRoutePair + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return PeerRoutePairImpl.frbInternalDcoDecode(raw as List); + } + + @protected + Route + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return RouteImpl.frbInternalDcoDecode(raw as List); + } + + @protected + String dco_decode_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as String; + } + + @protected + bool dco_decode_bool(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as bool; + } + + @protected + double dco_decode_f_64(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as double; + } + + @protected + int dco_decode_i_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + KVNetworkStatus dco_decode_kv_network_status(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return KVNetworkStatus( + totalNodes: dco_decode_usize(arr[0]), + nodes: dco_decode_list_kv_node_info(arr[1]), + ); + } + + @protected + KVNodeConnectionStats dco_decode_kv_node_connection_stats(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); + return KVNodeConnectionStats( + connType: dco_decode_String(arr[0]), + rxBytes: dco_decode_u_64(arr[1]), + txBytes: dco_decode_u_64(arr[2]), + rxPackets: dco_decode_u_64(arr[3]), + txPackets: dco_decode_u_64(arr[4]), + ); + } + + @protected + KVNodeInfo dco_decode_kv_node_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 6) + throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); + return KVNodeInfo( + hostname: dco_decode_String(arr[0]), + ipv4: dco_decode_String(arr[1]), + latencyMs: dco_decode_f_64(arr[2]), + connections: dco_decode_list_kv_node_connection_stats(arr[3]), + version: dco_decode_String(arr[4]), + cost: dco_decode_i_32(arr[5]), + ); + } + + @protected + List + dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List) + .map( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo) + .toList(); + } + + @protected + List + dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List) + .map( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair) + .toList(); + } + + @protected + List + dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List) + .map( + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute) + .toList(); + } + + @protected + List dco_decode_list_kv_node_connection_stats( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List) + .map(dco_decode_kv_node_connection_stats) + .toList(); + } + + @protected + List dco_decode_list_kv_node_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_kv_node_info).toList(); + } + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as Uint8List; + } + + @protected + ( + List, + List + ) dco_decode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + arr[0]), + dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + arr[1]), + ); + } + + @protected + BigInt dco_decode_u_64(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dcoDecodeU64(raw); + } + + @protected + int dco_decode_u_8(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + void dco_decode_unit(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return; + } + + @protected + BigInt dco_decode_usize(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dcoDecodeU64(raw); + } + + @protected + MyNodeInfo + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return MyNodeInfoImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), sse_decode_i_32(deserializer)); + } + + @protected + PeerInfo + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return PeerInfoImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), sse_decode_i_32(deserializer)); + } + + @protected + PeerRoutePair + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return PeerRoutePairImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), sse_decode_i_32(deserializer)); + } + + @protected + Route + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return RouteImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), sse_decode_i_32(deserializer)); + } + + @protected + MyNodeInfo + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return MyNodeInfoImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), sse_decode_i_32(deserializer)); + } + + @protected + PeerInfo + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return PeerInfoImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), sse_decode_i_32(deserializer)); + } + + @protected + PeerRoutePair + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return PeerRoutePairImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), sse_decode_i_32(deserializer)); + } + + @protected + Route + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return RouteImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), sse_decode_i_32(deserializer)); + } + + @protected + String sse_decode_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_prim_u_8_strict(deserializer); + return utf8.decoder.convert(inner); + } + + @protected + bool sse_decode_bool(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint8() != 0; + } + + @protected + double sse_decode_f_64(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getFloat64(); + } + + @protected + int sse_decode_i_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getInt32(); + } + + @protected + KVNetworkStatus sse_decode_kv_network_status(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_totalNodes = sse_decode_usize(deserializer); + var var_nodes = sse_decode_list_kv_node_info(deserializer); + return KVNetworkStatus(totalNodes: var_totalNodes, nodes: var_nodes); + } + + @protected + KVNodeConnectionStats sse_decode_kv_node_connection_stats( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_connType = sse_decode_String(deserializer); + var var_rxBytes = sse_decode_u_64(deserializer); + var var_txBytes = sse_decode_u_64(deserializer); + var var_rxPackets = sse_decode_u_64(deserializer); + var var_txPackets = sse_decode_u_64(deserializer); + return KVNodeConnectionStats( + connType: var_connType, + rxBytes: var_rxBytes, + txBytes: var_txBytes, + rxPackets: var_rxPackets, + txPackets: var_txPackets); + } + + @protected + KVNodeInfo sse_decode_kv_node_info(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_hostname = sse_decode_String(deserializer); + var var_ipv4 = sse_decode_String(deserializer); + var var_latencyMs = sse_decode_f_64(deserializer); + var var_connections = + sse_decode_list_kv_node_connection_stats(deserializer); + var var_version = sse_decode_String(deserializer); + var var_cost = sse_decode_i_32(deserializer); + return KVNodeInfo( + hostname: var_hostname, + ipv4: var_ipv4, + latencyMs: var_latencyMs, + connections: var_connections, + version: var_version, + cost: var_cost); + } + + @protected + List + sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add( + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + deserializer)); + } + return ans_; + } + + @protected + List + sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add( + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + deserializer)); + } + return ans_; + } + + @protected + List + sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add( + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + deserializer)); + } + return ans_; + } + + @protected + List sse_decode_list_kv_node_connection_stats( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_kv_node_connection_stats(deserializer)); + } + return ans_; + } + + @protected + List sse_decode_list_kv_node_info(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_kv_node_info(deserializer)); + } + return ans_; + } + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getUint8List(len_); + } + + @protected + ( + List, + List + ) sse_decode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = + sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + deserializer); + var var_field1 = + sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + deserializer); + return (var_field0, var_field1); + } + + @protected + BigInt sse_decode_u_64(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getBigUint64(); + } + + @protected + int sse_decode_u_8(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint8(); + } + + @protected + void sse_decode_unit(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getBigUint64(); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + MyNodeInfo self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as MyNodeInfoImpl).frbInternalSseEncode(move: true), serializer); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + PeerInfo self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as PeerInfoImpl).frbInternalSseEncode(move: true), serializer); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + PeerRoutePair self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as PeerRoutePairImpl).frbInternalSseEncode(move: true), + serializer); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + Route self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as RouteImpl).frbInternalSseEncode(move: true), serializer); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + MyNodeInfo self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as MyNodeInfoImpl).frbInternalSseEncode(move: null), serializer); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + PeerInfo self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as PeerInfoImpl).frbInternalSseEncode(move: null), serializer); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + PeerRoutePair self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as PeerRoutePairImpl).frbInternalSseEncode(move: null), + serializer); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + Route self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as RouteImpl).frbInternalSseEncode(move: null), serializer); + } + + @protected + void sse_encode_String(String self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); + } + + @protected + void sse_encode_bool(bool self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint8(self ? 1 : 0); + } + + @protected + void sse_encode_f_64(double self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putFloat64(self); + } + + @protected + void sse_encode_i_32(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putInt32(self); + } + + @protected + void sse_encode_kv_network_status( + KVNetworkStatus self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize(self.totalNodes, serializer); + sse_encode_list_kv_node_info(self.nodes, serializer); + } + + @protected + void sse_encode_kv_node_connection_stats( + KVNodeConnectionStats self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.connType, serializer); + sse_encode_u_64(self.rxBytes, serializer); + sse_encode_u_64(self.txBytes, serializer); + sse_encode_u_64(self.rxPackets, serializer); + sse_encode_u_64(self.txPackets, serializer); + } + + @protected + void sse_encode_kv_node_info(KVNodeInfo self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.hostname, serializer); + sse_encode_String(self.ipv4, serializer); + sse_encode_f_64(self.latencyMs, serializer); + sse_encode_list_kv_node_connection_stats(self.connections, serializer); + sse_encode_String(self.version, serializer); + sse_encode_i_32(self.cost, serializer); + } + + @protected + void + sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + item, serializer); + } + } + + @protected + void + sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + item, serializer); + } + } + + @protected + void + sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + item, serializer); + } + } + + @protected + void sse_encode_list_kv_node_connection_stats( + List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_kv_node_connection_stats(item, serializer); + } + } + + @protected + void sse_encode_list_kv_node_info( + List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_kv_node_info(item, serializer); + } + } + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putUint8List(self); + } + + @protected + void + sse_encode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route( + (List, List) self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + self.$1, serializer); + sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + self.$2, serializer); + } + + @protected + void sse_encode_u_64(BigInt self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putBigUint64(self); + } + + @protected + void sse_encode_u_8(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint8(self); + } + + @protected + void sse_encode_unit(void self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putBigUint64(self); + } +} + +@sealed +class MyNodeInfoImpl extends RustOpaque implements MyNodeInfo { + // Not to be used by end users + MyNodeInfoImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + MyNodeInfoImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_MyNodeInfo, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_MyNodeInfo, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_MyNodeInfoPtr, + ); +} + +@sealed +class PeerInfoImpl extends RustOpaque implements PeerInfo { + // Not to be used by end users + PeerInfoImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + PeerInfoImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_PeerInfo, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_PeerInfo, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_PeerInfoPtr, + ); +} + +@sealed +class PeerRoutePairImpl extends RustOpaque implements PeerRoutePair { + // Not to be used by end users + PeerRoutePairImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + PeerRoutePairImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_PeerRoutePair, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_PeerRoutePair, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_PeerRoutePairPtr, + ); +} + +@sealed +class RouteImpl extends RustOpaque implements Route { + // Not to be used by end users + RouteImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + RouteImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_Route, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_Route, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_RoutePtr, + ); +} diff --git a/lib/src/rust/frb_generated.io.dart b/lib/src/rust/frb_generated.io.dart new file mode 100644 index 0000000..e516831 --- /dev/null +++ b/lib/src/rust/frb_generated.io.dart @@ -0,0 +1,494 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +import 'api/simple.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi' as ffi; +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; + +abstract class RustLibApiImplPlatform extends BaseApiImpl { + RustLibApiImplPlatform({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_MyNodeInfoPtr => + wire._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_PeerInfoPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_PeerRoutePairPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RoutePtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr; + + @protected + MyNodeInfo + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + dynamic raw); + + @protected + PeerInfo + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + dynamic raw); + + @protected + PeerRoutePair + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + dynamic raw); + + @protected + Route + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + dynamic raw); + + @protected + MyNodeInfo + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + dynamic raw); + + @protected + PeerInfo + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + dynamic raw); + + @protected + PeerRoutePair + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + dynamic raw); + + @protected + Route + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + dynamic raw); + + @protected + String dco_decode_String(dynamic raw); + + @protected + bool dco_decode_bool(dynamic raw); + + @protected + double dco_decode_f_64(dynamic raw); + + @protected + int dco_decode_i_32(dynamic raw); + + @protected + KVNetworkStatus dco_decode_kv_network_status(dynamic raw); + + @protected + KVNodeConnectionStats dco_decode_kv_node_connection_stats(dynamic raw); + + @protected + KVNodeInfo dco_decode_kv_node_info(dynamic raw); + + @protected + List + dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + dynamic raw); + + @protected + List + dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + dynamic raw); + + @protected + List + dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + dynamic raw); + + @protected + List dco_decode_list_kv_node_connection_stats( + dynamic raw); + + @protected + List dco_decode_list_kv_node_info(dynamic raw); + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + + @protected + ( + List, + List + ) dco_decode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route( + dynamic raw); + + @protected + BigInt dco_decode_u_64(dynamic raw); + + @protected + int dco_decode_u_8(dynamic raw); + + @protected + void dco_decode_unit(dynamic raw); + + @protected + BigInt dco_decode_usize(dynamic raw); + + @protected + MyNodeInfo + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + SseDeserializer deserializer); + + @protected + PeerInfo + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + SseDeserializer deserializer); + + @protected + PeerRoutePair + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + SseDeserializer deserializer); + + @protected + Route + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + SseDeserializer deserializer); + + @protected + MyNodeInfo + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + SseDeserializer deserializer); + + @protected + PeerInfo + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + SseDeserializer deserializer); + + @protected + PeerRoutePair + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + SseDeserializer deserializer); + + @protected + Route + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + SseDeserializer deserializer); + + @protected + String sse_decode_String(SseDeserializer deserializer); + + @protected + bool sse_decode_bool(SseDeserializer deserializer); + + @protected + double sse_decode_f_64(SseDeserializer deserializer); + + @protected + int sse_decode_i_32(SseDeserializer deserializer); + + @protected + KVNetworkStatus sse_decode_kv_network_status(SseDeserializer deserializer); + + @protected + KVNodeConnectionStats sse_decode_kv_node_connection_stats( + SseDeserializer deserializer); + + @protected + KVNodeInfo sse_decode_kv_node_info(SseDeserializer deserializer); + + @protected + List + sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + SseDeserializer deserializer); + + @protected + List + sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + SseDeserializer deserializer); + + @protected + List + sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + SseDeserializer deserializer); + + @protected + List sse_decode_list_kv_node_connection_stats( + SseDeserializer deserializer); + + @protected + List sse_decode_list_kv_node_info(SseDeserializer deserializer); + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + ( + List, + List + ) sse_decode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route( + SseDeserializer deserializer); + + @protected + BigInt sse_decode_u_64(SseDeserializer deserializer); + + @protected + int sse_decode_u_8(SseDeserializer deserializer); + + @protected + void sse_decode_unit(SseDeserializer deserializer); + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + MyNodeInfo self, SseSerializer serializer); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + PeerInfo self, SseSerializer serializer); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + PeerRoutePair self, SseSerializer serializer); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + Route self, SseSerializer serializer); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + MyNodeInfo self, SseSerializer serializer); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + PeerInfo self, SseSerializer serializer); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + PeerRoutePair self, SseSerializer serializer); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + Route self, SseSerializer serializer); + + @protected + void sse_encode_String(String self, SseSerializer serializer); + + @protected + void sse_encode_bool(bool self, SseSerializer serializer); + + @protected + void sse_encode_f_64(double self, SseSerializer serializer); + + @protected + void sse_encode_i_32(int self, SseSerializer serializer); + + @protected + void sse_encode_kv_network_status( + KVNetworkStatus self, SseSerializer serializer); + + @protected + void sse_encode_kv_node_connection_stats( + KVNodeConnectionStats self, SseSerializer serializer); + + @protected + void sse_encode_kv_node_info(KVNodeInfo self, SseSerializer serializer); + + @protected + void + sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + List self, SseSerializer serializer); + + @protected + void + sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + List self, SseSerializer serializer); + + @protected + void + sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + List self, SseSerializer serializer); + + @protected + void sse_encode_list_kv_node_connection_stats( + List self, SseSerializer serializer); + + @protected + void sse_encode_list_kv_node_info( + List self, SseSerializer serializer); + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, SseSerializer serializer); + + @protected + void + sse_encode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route( + (List, List) self, SseSerializer serializer); + + @protected + void sse_encode_u_64(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_u_8(int self, SseSerializer serializer); + + @protected + void sse_encode_unit(void self, SseSerializer serializer); + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer); +} + +// Section: wire_class + +class RustLibWire implements BaseWire { + factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) => + RustLibWire(lib.ffiDynamicLibrary); + + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + RustLibWire(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr = + _lookup)>>( + 'frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo'); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr = + _lookup)>>( + 'frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo'); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfoPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr = + _lookup)>>( + 'frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo'); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr = + _lookup)>>( + 'frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo'); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfoPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr = + _lookup)>>( + 'frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair'); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr = + _lookup)>>( + 'frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair'); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePairPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr = + _lookup)>>( + 'frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute'); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr = + _lookup)>>( + 'frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute'); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoutePtr + .asFunction)>(); +} diff --git a/lib/src/rust/frb_generated.web.dart b/lib/src/rust/frb_generated.web.dart new file mode 100644 index 0000000..1b7520a --- /dev/null +++ b/lib/src/rust/frb_generated.web.dart @@ -0,0 +1,446 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +// Static analysis wrongly picks the IO variant, thus ignore this +// ignore_for_file: argument_type_not_assignable + +import 'api/simple.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; + +abstract class RustLibApiImplPlatform extends BaseApiImpl { + RustLibApiImplPlatform({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_MyNodeInfoPtr => + wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_PeerInfoPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_PeerRoutePairPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_RoutePtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute; + + @protected + MyNodeInfo + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + dynamic raw); + + @protected + PeerInfo + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + dynamic raw); + + @protected + PeerRoutePair + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + dynamic raw); + + @protected + Route + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + dynamic raw); + + @protected + MyNodeInfo + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + dynamic raw); + + @protected + PeerInfo + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + dynamic raw); + + @protected + PeerRoutePair + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + dynamic raw); + + @protected + Route + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + dynamic raw); + + @protected + String dco_decode_String(dynamic raw); + + @protected + bool dco_decode_bool(dynamic raw); + + @protected + double dco_decode_f_64(dynamic raw); + + @protected + int dco_decode_i_32(dynamic raw); + + @protected + KVNetworkStatus dco_decode_kv_network_status(dynamic raw); + + @protected + KVNodeConnectionStats dco_decode_kv_node_connection_stats(dynamic raw); + + @protected + KVNodeInfo dco_decode_kv_node_info(dynamic raw); + + @protected + List + dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + dynamic raw); + + @protected + List + dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + dynamic raw); + + @protected + List + dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + dynamic raw); + + @protected + List dco_decode_list_kv_node_connection_stats( + dynamic raw); + + @protected + List dco_decode_list_kv_node_info(dynamic raw); + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + + @protected + ( + List, + List + ) dco_decode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route( + dynamic raw); + + @protected + BigInt dco_decode_u_64(dynamic raw); + + @protected + int dco_decode_u_8(dynamic raw); + + @protected + void dco_decode_unit(dynamic raw); + + @protected + BigInt dco_decode_usize(dynamic raw); + + @protected + MyNodeInfo + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + SseDeserializer deserializer); + + @protected + PeerInfo + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + SseDeserializer deserializer); + + @protected + PeerRoutePair + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + SseDeserializer deserializer); + + @protected + Route + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + SseDeserializer deserializer); + + @protected + MyNodeInfo + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + SseDeserializer deserializer); + + @protected + PeerInfo + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + SseDeserializer deserializer); + + @protected + PeerRoutePair + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + SseDeserializer deserializer); + + @protected + Route + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + SseDeserializer deserializer); + + @protected + String sse_decode_String(SseDeserializer deserializer); + + @protected + bool sse_decode_bool(SseDeserializer deserializer); + + @protected + double sse_decode_f_64(SseDeserializer deserializer); + + @protected + int sse_decode_i_32(SseDeserializer deserializer); + + @protected + KVNetworkStatus sse_decode_kv_network_status(SseDeserializer deserializer); + + @protected + KVNodeConnectionStats sse_decode_kv_node_connection_stats( + SseDeserializer deserializer); + + @protected + KVNodeInfo sse_decode_kv_node_info(SseDeserializer deserializer); + + @protected + List + sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + SseDeserializer deserializer); + + @protected + List + sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + SseDeserializer deserializer); + + @protected + List + sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + SseDeserializer deserializer); + + @protected + List sse_decode_list_kv_node_connection_stats( + SseDeserializer deserializer); + + @protected + List sse_decode_list_kv_node_info(SseDeserializer deserializer); + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + ( + List, + List + ) sse_decode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route( + SseDeserializer deserializer); + + @protected + BigInt sse_decode_u_64(SseDeserializer deserializer); + + @protected + int sse_decode_u_8(SseDeserializer deserializer); + + @protected + void sse_decode_unit(SseDeserializer deserializer); + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + MyNodeInfo self, SseSerializer serializer); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + PeerInfo self, SseSerializer serializer); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + PeerRoutePair self, SseSerializer serializer); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + Route self, SseSerializer serializer); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + MyNodeInfo self, SseSerializer serializer); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + PeerInfo self, SseSerializer serializer); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + PeerRoutePair self, SseSerializer serializer); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + Route self, SseSerializer serializer); + + @protected + void sse_encode_String(String self, SseSerializer serializer); + + @protected + void sse_encode_bool(bool self, SseSerializer serializer); + + @protected + void sse_encode_f_64(double self, SseSerializer serializer); + + @protected + void sse_encode_i_32(int self, SseSerializer serializer); + + @protected + void sse_encode_kv_network_status( + KVNetworkStatus self, SseSerializer serializer); + + @protected + void sse_encode_kv_node_connection_stats( + KVNodeConnectionStats self, SseSerializer serializer); + + @protected + void sse_encode_kv_node_info(KVNodeInfo self, SseSerializer serializer); + + @protected + void + sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + List self, SseSerializer serializer); + + @protected + void + sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + List self, SseSerializer serializer); + + @protected + void + sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + List self, SseSerializer serializer); + + @protected + void sse_encode_list_kv_node_connection_stats( + List self, SseSerializer serializer); + + @protected + void sse_encode_list_kv_node_info( + List self, SseSerializer serializer); + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, SseSerializer serializer); + + @protected + void + sse_encode_record_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_peer_info_list_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_route( + (List, List) self, SseSerializer serializer); + + @protected + void sse_encode_u_64(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_u_8(int self, SseSerializer serializer); + + @protected + void sse_encode_unit(void self, SseSerializer serializer); + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer); +} + +// Section: wire_class + +class RustLibWire implements BaseWire { + RustLibWire.fromExternalLibrary(ExternalLibrary lib); + + void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + int ptr) => + wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + ptr); + + void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + int ptr) => + wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + ptr); + + void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + int ptr) => + wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + ptr); + + void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + int ptr) => + wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + ptr); + + void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + int ptr) => + wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + ptr); + + void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + int ptr) => + wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + ptr); + + void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + int ptr) => + wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + ptr); + + void rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + int ptr) => + wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + ptr); +} + +@JS('wasm_bindgen') +external RustLibWasmModule get wasmModule; + +@JS() +@anonymous +extension type RustLibWasmModule._(JSObject _) implements JSObject { + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + int ptr); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + int ptr); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + int ptr); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + int ptr); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + int ptr); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + int ptr); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + int ptr); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + int ptr); +} diff --git a/lib/utils/app_info.dart b/lib/utils/app_info.dart new file mode 100644 index 0000000..ef8370d --- /dev/null +++ b/lib/utils/app_info.dart @@ -0,0 +1,37 @@ +import 'package:package_info_plus/package_info_plus.dart'; + +class AppInfoUtil { + static PackageInfo? _packageInfo; + + /// 初始化应用信息 + static Future init() async { + _packageInfo = await PackageInfo.fromPlatform(); + } + + /// 获取应用版本号 (例如: 1.0.0) + static String getVersion() { + return _packageInfo?.version ?? ''; + } + + /// 获取应用构建号 (例如: 1) + static String getBuildNumber() { + return _packageInfo?.buildNumber ?? ''; + } + + /// 获取完整版本号 (例如: 1.0.0+1) + static String getFullVersion() { + final version = getVersion(); + final buildNumber = getBuildNumber(); + return '$version+$buildNumber'; + } + + /// 获取应用名称 + static String getAppName() { + return _packageInfo?.appName ?? ''; + } + + /// 获取包名 + static String getPackageName() { + return _packageInfo?.packageName ?? ''; + } +} diff --git a/lib/utils/cs.json b/lib/utils/cs.json new file mode 100644 index 0000000..d28c8c7 --- /dev/null +++ b/lib/utils/cs.json @@ -0,0 +1 @@ +{"dev_name":"et_9_d759","error_msg":null,"events":["{\"time\":\"2025-03-14T18:13:01.308506900+08:00\",\"event\":{\"PeerAdded\":1866782266}}","{\"time\":\"2025-03-14T18:13:01.308492700+08:00\",\"event\":{\"PeerConnAdded\":{\"conn_id\":\"9b0c7f85-9c93-455e-a24d-d48c7c514fce\",\"my_peer_id\":2385160605,\"peer_id\":1866782266,\"features\":[],\"tunnel\":{\"tunnel_type\":\"udp\",\"local_addr\":{\"url\":\"udp://0.0.0.0:60679\"},\"remote_addr\":{\"url\":\"udp://123.132.212.58:12456\"}},\"stats\":{\"rx_bytes\":77,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":false,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:57.786576600+08:00\",\"event\":{\"PeerConnRemoved\":{\"conn_id\":\"e74bd54d-3633-41c8-adb7-caac62c5e7a9\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://172.29.176.1:2904\"},\"remote_addr\":{\"url\":\"tcp://172.29.176.1:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:57.786396200+08:00\",\"event\":{\"PeerConnAdded\":{\"conn_id\":\"e74bd54d-3633-41c8-adb7-caac62c5e7a9\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://172.29.176.1:2904\"},\"remote_addr\":{\"url\":\"tcp://172.29.176.1:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:55.495366400+08:00\",\"event\":{\"PeerConnRemoved\":{\"conn_id\":\"30bdadcd-6133-48d6-9bff-33fa9e8beec1\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://192.168.110.140:2847\"},\"remote_addr\":{\"url\":\"tcp://192.168.110.140:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:55.495211500+08:00\",\"event\":{\"PeerConnAdded\":{\"conn_id\":\"30bdadcd-6133-48d6-9bff-33fa9e8beec1\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://192.168.110.140:2847\"},\"remote_addr\":{\"url\":\"tcp://192.168.110.140:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:54.645060200+08:00\",\"event\":{\"PeerConnRemoved\":{\"conn_id\":\"7542eaa6-62f3-432a-a7bb-b1edc257d8d6\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://26.79.171.253:2832\"},\"remote_addr\":{\"url\":\"tcp://26.79.171.253:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:54.644862+08:00\",\"event\":{\"PeerConnAdded\":{\"conn_id\":\"7542eaa6-62f3-432a-a7bb-b1edc257d8d6\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://26.79.171.253:2832\"},\"remote_addr\":{\"url\":\"tcp://26.79.171.253:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:52.429549800+08:00\",\"event\":{\"PeerConnRemoved\":{\"conn_id\":\"e83f1e11-1ae1-4c1f-8468-a73b5f7515b3\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://192.168.110.140:2777\"},\"remote_addr\":{\"url\":\"tcp://192.168.110.140:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:52.429530400+08:00\",\"event\":{\"PeerConnAdded\":{\"conn_id\":\"e83f1e11-1ae1-4c1f-8468-a73b5f7515b3\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://192.168.110.140:2777\"},\"remote_addr\":{\"url\":\"tcp://192.168.110.140:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:51.870442900+08:00\",\"event\":{\"PeerConnRemoved\":{\"conn_id\":\"f0a6644f-e2cc-4291-88c7-d13e6209bc83\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://172.29.176.1:2751\"},\"remote_addr\":{\"url\":\"tcp://172.29.176.1:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:51.870429600+08:00\",\"event\":{\"PeerConnAdded\":{\"conn_id\":\"f0a6644f-e2cc-4291-88c7-d13e6209bc83\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://172.29.176.1:2751\"},\"remote_addr\":{\"url\":\"tcp://172.29.176.1:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:51.225096600+08:00\",\"event\":{\"PeerConnRemoved\":{\"conn_id\":\"d4e23e2b-3454-418b-a25f-efffb12a23c5\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://26.79.171.253:2733\"},\"remote_addr\":{\"url\":\"tcp://26.79.171.253:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:51.225080600+08:00\",\"event\":{\"PeerConnAdded\":{\"conn_id\":\"d4e23e2b-3454-418b-a25f-efffb12a23c5\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://26.79.171.253:2733\"},\"remote_addr\":{\"url\":\"tcp://26.79.171.253:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:50.464672500+08:00\",\"event\":{\"PeerConnRemoved\":{\"conn_id\":\"75239972-4127-4f55-83a1-ea094e8025b3\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://192.168.110.140:2712\"},\"remote_addr\":{\"url\":\"tcp://192.168.110.140:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:50.464651100+08:00\",\"event\":{\"PeerConnAdded\":{\"conn_id\":\"75239972-4127-4f55-83a1-ea094e8025b3\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://192.168.110.140:2712\"},\"remote_addr\":{\"url\":\"tcp://192.168.110.140:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:50.305469800+08:00\",\"event\":{\"PeerConnRemoved\":{\"conn_id\":\"3bfe93bb-f434-4fd9-81a5-39347b8b36af\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://172.29.176.1:2702\"},\"remote_addr\":{\"url\":\"tcp://172.29.176.1:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:50.303905200+08:00\",\"event\":{\"PeerConnAdded\":{\"conn_id\":\"3bfe93bb-f434-4fd9-81a5-39347b8b36af\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://172.29.176.1:2702\"},\"remote_addr\":{\"url\":\"tcp://172.29.176.1:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:49.984709500+08:00\",\"event\":{\"PeerConnRemoved\":{\"conn_id\":\"cf871a1e-2bed-48bd-a596-c87c329f213d\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://26.79.171.253:2697\"},\"remote_addr\":{\"url\":\"tcp://26.79.171.253:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}","{\"time\":\"2025-03-14T18:11:49.984554400+08:00\",\"event\":{\"PeerConnAdded\":{\"conn_id\":\"cf871a1e-2bed-48bd-a596-c87c329f213d\",\"my_peer_id\":2385160605,\"peer_id\":3202873479,\"features\":[],\"tunnel\":{\"tunnel_type\":\"tcp\",\"local_addr\":{\"url\":\"tcp://26.79.171.253:2697\"},\"remote_addr\":{\"url\":\"tcp://26.79.171.253:11010\"}},\"stats\":{\"rx_bytes\":73,\"tx_bytes\":89,\"rx_packets\":1,\"tx_packets\":1,\"latency_us\":0},\"loss_rate\":0.0,\"is_client\":true,\"network_name\":\"abc\"}}}"],"my_node_info":{"hostname":"用户11","ips":{"interface_ipv4s":[{"addr":2887626753},{"addr":3232263820},{"addr":441428989}],"interface_ipv6s":[],"listeners":[],"public_ipv4":{"addr":2072302650},"public_ipv6":null},"listeners":[{"url":"ring://7ece8ca3-82f2-4a97-bf9c-f57443f6bb30"},{"url":"tcp://0.0.0.0:11010"},{"url":"udp://0.0.0.0:11010"},{"url":"udp://[::]:65437"}],"stun_info":{"last_update_time":1741946795,"max_port":25735,"min_port":25735,"public_ip":["123.132.212.58"],"tcp_nat_type":0,"udp_nat_type":5},"version":"2.2.2-79f3427c~","virtual_ipv4":{"address":{"addr":177246214},"network_length":24},"vpn_portal_cfg":"ERROR: Wireguard VPN Portal Not Started"},"routes":[{"ipv4_addr":{"address":{"addr":177246218},"network_length":24},"peer_id":2941745776},{"ipv4_addr":null,"peer_id":614551919},{"ipv4_addr":{"address":{"addr":177246211},"network_length":24},"peer_id":1891771079},{"ipv4_addr":{"address":{"addr":177246217},"network_length":24},"peer_id":4109660553},{"ipv4_addr":{"address":{"addr":177246210},"network_length":24},"peer_id":2981076496},{"ipv4_addr":{"address":{"addr":177246215},"network_length":24},"peer_id":4196431329},{"ipv4_addr":{"address":{"addr":177246216},"network_length":24},"peer_id":3111714925},{"ipv4_addr":{"address":{"addr":177246209},"network_length":24},"peer_id":1209643366},{"ipv4_addr":{"address":{"addr":177246220},"network_length":24},"peer_id":1866782266},{"ipv4_addr":{"address":{"addr":177246213},"network_length":24},"peer_id":1105918216},{"ipv4_addr":{"address":{"addr":177246219},"network_length":24},"peer_id":1362482027},{"ipv4_addr":{"address":{"addr":177246212},"network_length":24},"peer_id":3202873479}],"running":true} \ No newline at end of file diff --git a/lib/utils/ping_util.dart b/lib/utils/ping_util.dart new file mode 100644 index 0000000..5f784b6 --- /dev/null +++ b/lib/utils/ping_util.dart @@ -0,0 +1,27 @@ +import 'dart:io'; +import 'dart:async'; + +class PingUtil { + static Future ping(String host) async { + try { + // 从 host:port 格式中提取主机名 + final hostname = host.split(':')[0]; + final post = host.split(':')[1]; + + // 使用 Socket 连接来测量实际网络延迟 + final startTime = DateTime.now(); + final socket = await Socket.connect(hostname, int.parse(post), + timeout: const Duration(seconds: 2)); + final endTime = DateTime.now(); + + // 关闭连接 + await socket.close(); + + return endTime.difference(startTime).inMilliseconds; + } on SocketException { + return null; + } catch (e) { + return null; + } + } +} diff --git a/lib/utils/runin.dart b/lib/utils/runin.dart new file mode 100644 index 0000000..b8d1223 --- /dev/null +++ b/lib/utils/runin.dart @@ -0,0 +1,357 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'runin.g.dart'; + +@JsonSerializable() +class Ipv4Addr { + final int addr; + + Ipv4Addr({required this.addr}); + + factory Ipv4Addr.fromJson(Map json) => + _$Ipv4AddrFromJson(json); + Map toJson() => _$Ipv4AddrToJson(this); +} + +@JsonSerializable() +class Ipv4Inet { + @JsonKey(name: 'address') + final Ipv4Addr? address; + @JsonKey(name: 'network_length') + final int networkLength; + + Ipv4Inet({this.address, required this.networkLength}); + + factory Ipv4Inet.fromJson(Map json) => + _$Ipv4InetFromJson(json); + Map toJson() => _$Ipv4InetToJson(this); +} + +@JsonSerializable() +class StunInfo { + @JsonKey(name: 'udp_nat_type') + final int udpNatType; + @JsonKey(name: 'tcp_nat_type') + final int tcpNatType; + @JsonKey(name: 'last_update_time') + final int lastUpdateTime; + @JsonKey(name: 'public_ip') + final List publicIp; + @JsonKey(name: 'min_port') + final int minPort; + @JsonKey(name: 'max_port') + final int maxPort; + + StunInfo({ + required this.udpNatType, + required this.tcpNatType, + required this.lastUpdateTime, + required this.publicIp, + required this.minPort, + required this.maxPort, + }); + + factory StunInfo.fromJson(Map json) => + _$StunInfoFromJson(json); + Map toJson() => _$StunInfoToJson(this); +} + +@JsonSerializable() +class PeerFeatureFlag { + @JsonKey(name: 'is_public_server') + final bool isPublicServer; + @JsonKey(name: 'avoid_relay_data') + final bool avoidRelayData; + @JsonKey(name: 'kcp_input') + final bool kcpInput; + @JsonKey(name: 'no_relay_kcp') + final bool noRelayKcp; + + PeerFeatureFlag({ + required this.isPublicServer, + required this.avoidRelayData, + required this.kcpInput, + required this.noRelayKcp, + }); + + factory PeerFeatureFlag.fromJson(Map json) => + _$PeerFeatureFlagFromJson(json); + Map toJson() => _$PeerFeatureFlagToJson(this); +} + +@JsonSerializable() +class Url { + final String url; + + Url({required this.url}); + + factory Url.fromJson(Map json) => _$UrlFromJson(json); + Map toJson() => _$UrlToJson(this); +} + +@JsonSerializable() +class Route { + @JsonKey(name: 'peer_id') + final int? peerId; + @JsonKey(name: 'ipv4_addr') + final Ipv4Inet? ipv4Addr; + @JsonKey(name: 'next_hop_peer_id') + final int? nextHopPeerId; + final int? cost; + @JsonKey(name: 'path_latency') + final int? pathLatency; + @JsonKey(name: 'proxy_cidrs') + final List? proxyCidrs; + final String? hostname; + @JsonKey(name: 'stun_info') + final StunInfo? stunInfo; + @JsonKey(name: 'inst_id') + final String? instId; + final String? version; + @JsonKey(name: 'feature_flag') + final PeerFeatureFlag? featureFlag; + @JsonKey(name: 'next_hop_peer_id_latency_first') + final int? nextHopPeerIdLatencyFirst; + @JsonKey(name: 'cost_latency_first') + final int? costLatencyFirst; + @JsonKey(name: 'path_latency_latency_first') + final int? pathLatencyLatencyFirst; + + Route({ + this.peerId, + this.ipv4Addr, + this.nextHopPeerId, + this.cost, + this.pathLatency, + this.proxyCidrs, + this.hostname, + this.stunInfo, + this.instId, + this.version, + this.featureFlag, + this.nextHopPeerIdLatencyFirst, + this.costLatencyFirst, + this.pathLatencyLatencyFirst, + }); + + factory Route.fromJson(Map json) => _$RouteFromJson(json); + Map toJson() => _$RouteToJson(this); +} + +@JsonSerializable() +class TunnelInfo { + @JsonKey(name: 'tunnel_type') + final String tunnelType; + @JsonKey(name: 'local_addr') + final Url? localAddr; + @JsonKey(name: 'remote_addr') + final Url? remoteAddr; + + TunnelInfo({ + required this.tunnelType, + this.localAddr, + this.remoteAddr, + }); + + factory TunnelInfo.fromJson(Map json) => + _$TunnelInfoFromJson(json); + Map toJson() => _$TunnelInfoToJson(this); +} + +@JsonSerializable() +class PeerConnStats { + @JsonKey(name: 'rx_bytes') + final int rxBytes; + @JsonKey(name: 'tx_bytes') + final int txBytes; + @JsonKey(name: 'rx_packets') + final int rxPackets; + @JsonKey(name: 'tx_packets') + final int txPackets; + @JsonKey(name: 'latency_us') + final int latencyUs; + + PeerConnStats({ + required this.rxBytes, + required this.txBytes, + required this.rxPackets, + required this.txPackets, + required this.latencyUs, + }); + + factory PeerConnStats.fromJson(Map json) => + _$PeerConnStatsFromJson(json); + Map toJson() => _$PeerConnStatsToJson(this); +} + +@JsonSerializable() +class PeerConnInfo { + @JsonKey(name: 'conn_id') + final String connId; + @JsonKey(name: 'my_peer_id') + final int myPeerId; + @JsonKey(name: 'peer_id') + final int peerId; + final List features; + final TunnelInfo? tunnel; + final PeerConnStats? stats; + @JsonKey(name: 'loss_rate') + final double lossRate; + @JsonKey(name: 'is_client') + final bool isClient; + @JsonKey(name: 'network_name') + final String networkName; + + PeerConnInfo({ + required this.connId, + required this.myPeerId, + required this.peerId, + required this.features, + this.tunnel, + this.stats, + required this.lossRate, + required this.isClient, + required this.networkName, + }); + + factory PeerConnInfo.fromJson(Map json) => + _$PeerConnInfoFromJson(json); + Map toJson() => _$PeerConnInfoToJson(this); +} + +@JsonSerializable() +class PeerInfo { + @JsonKey(name: 'peer_id') + final int peerId; + final List conns; + + PeerInfo({ + required this.peerId, + required this.conns, + }); + + factory PeerInfo.fromJson(Map json) => + _$PeerInfoFromJson(json); + Map toJson() => _$PeerInfoToJson(this); +} + +@JsonSerializable() +class PeerRoutePair { + final Route? route; + final PeerInfo? peer; + + PeerRoutePair({ + this.route, + this.peer, + }); + + factory PeerRoutePair.fromJson(Map json) => + _$PeerRoutePairFromJson(json); + Map toJson() => _$PeerRoutePairToJson(this); +} + +@JsonSerializable() +class Ipv6Addr { + @JsonKey(name: 'part1') + final int part1; + @JsonKey(name: 'part2') + final int part2; + @JsonKey(name: 'part3') + final int part3; + @JsonKey(name: 'part4') + final int part4; + + Ipv6Addr({ + required this.part1, + required this.part2, + required this.part3, + required this.part4, + }); + + factory Ipv6Addr.fromJson(Map json) => + _$Ipv6AddrFromJson(json); + Map toJson() => _$Ipv6AddrToJson(this); +} + +@JsonSerializable() +class GetIpListResponse { + @JsonKey(name: 'public_ipv4') + final Ipv4Addr? publicIpv4; + @JsonKey(name: 'interface_ipv4s') + final List interfaceIpv4s; + @JsonKey(name: 'public_ipv6') + final Ipv6Addr? publicIpv6; + @JsonKey(name: 'interface_ipv6s') + final List interfaceIpv6s; + final List listeners; + + GetIpListResponse({ + this.publicIpv4, + required this.interfaceIpv4s, + this.publicIpv6, + required this.interfaceIpv6s, + required this.listeners, + }); + + factory GetIpListResponse.fromJson(Map json) => + _$GetIpListResponseFromJson(json); + Map toJson() => _$GetIpListResponseToJson(this); +} + +@JsonSerializable() +class MyNodeInfo { + @JsonKey(name: 'virtual_ipv4') + final Ipv4Inet? virtualIpv4; + final String hostname; + final String version; + final GetIpListResponse? ips; + @JsonKey(name: 'stun_info') + final StunInfo? stunInfo; + final List listeners; + @JsonKey(name: 'vpn_portal_cfg') + final String? vpnPortalCfg; + + MyNodeInfo({ + this.virtualIpv4, + required this.hostname, + required this.version, + this.ips, + this.stunInfo, + required this.listeners, + this.vpnPortalCfg, + }); + + factory MyNodeInfo.fromJson(Map json) => + _$MyNodeInfoFromJson(json); + Map toJson() => _$MyNodeInfoToJson(this); +} + +@JsonSerializable() +class Runin { + @JsonKey(name: 'dev_name') + final String? devName; + @JsonKey(name: 'my_node_info') + final MyNodeInfo? myNodeInfo; + final List events; + final List routes; + final List? peers; + @JsonKey(name: 'peer_route_pairs') + final List? peerRoutePairs; + final bool running; + @JsonKey(name: 'error_msg') + final String? errorMsg; + + Runin({ + this.devName, + this.myNodeInfo, + required this.events, + required this.routes, + this.peers, + this.peerRoutePairs, + required this.running, + this.errorMsg, + }); + + factory Runin.fromJson(Map json) => _$RuninFromJson(json); + Map toJson() => _$RuninToJson(this); +} diff --git a/lib/utils/runin.g.dart b/lib/utils/runin.g.dart new file mode 100644 index 0000000..144d45d --- /dev/null +++ b/lib/utils/runin.g.dart @@ -0,0 +1,310 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'runin.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Ipv4Addr _$Ipv4AddrFromJson(Map json) => Ipv4Addr( + addr: (json['addr'] as num).toInt(), + ); + +Map _$Ipv4AddrToJson(Ipv4Addr instance) => { + 'addr': instance.addr, + }; + +Ipv4Inet _$Ipv4InetFromJson(Map json) => Ipv4Inet( + address: json['address'] == null + ? null + : Ipv4Addr.fromJson(json['address'] as Map), + networkLength: (json['network_length'] as num).toInt(), + ); + +Map _$Ipv4InetToJson(Ipv4Inet instance) => { + 'address': instance.address, + 'network_length': instance.networkLength, + }; + +StunInfo _$StunInfoFromJson(Map json) => StunInfo( + udpNatType: (json['udp_nat_type'] as num).toInt(), + tcpNatType: (json['tcp_nat_type'] as num).toInt(), + lastUpdateTime: (json['last_update_time'] as num).toInt(), + publicIp: + (json['public_ip'] as List).map((e) => e as String).toList(), + minPort: (json['min_port'] as num).toInt(), + maxPort: (json['max_port'] as num).toInt(), + ); + +Map _$StunInfoToJson(StunInfo instance) => { + 'udp_nat_type': instance.udpNatType, + 'tcp_nat_type': instance.tcpNatType, + 'last_update_time': instance.lastUpdateTime, + 'public_ip': instance.publicIp, + 'min_port': instance.minPort, + 'max_port': instance.maxPort, + }; + +PeerFeatureFlag _$PeerFeatureFlagFromJson(Map json) => + PeerFeatureFlag( + isPublicServer: json['is_public_server'] as bool, + avoidRelayData: json['avoid_relay_data'] as bool, + kcpInput: json['kcp_input'] as bool, + noRelayKcp: json['no_relay_kcp'] as bool, + ); + +Map _$PeerFeatureFlagToJson(PeerFeatureFlag instance) => + { + 'is_public_server': instance.isPublicServer, + 'avoid_relay_data': instance.avoidRelayData, + 'kcp_input': instance.kcpInput, + 'no_relay_kcp': instance.noRelayKcp, + }; + +Url _$UrlFromJson(Map json) => Url( + url: json['url'] as String, + ); + +Map _$UrlToJson(Url instance) => { + 'url': instance.url, + }; + +Route _$RouteFromJson(Map json) => Route( + peerId: (json['peer_id'] as num?)?.toInt(), + ipv4Addr: json['ipv4_addr'] == null + ? null + : Ipv4Inet.fromJson(json['ipv4_addr'] as Map), + nextHopPeerId: (json['next_hop_peer_id'] as num?)?.toInt(), + cost: (json['cost'] as num?)?.toInt(), + pathLatency: (json['path_latency'] as num?)?.toInt(), + proxyCidrs: (json['proxy_cidrs'] as List?) + ?.map((e) => e as String) + .toList(), + hostname: json['hostname'] as String?, + stunInfo: json['stun_info'] == null + ? null + : StunInfo.fromJson(json['stun_info'] as Map), + instId: json['inst_id'] as String?, + version: json['version'] as String?, + featureFlag: json['feature_flag'] == null + ? null + : PeerFeatureFlag.fromJson( + json['feature_flag'] as Map), + nextHopPeerIdLatencyFirst: + (json['next_hop_peer_id_latency_first'] as num?)?.toInt(), + costLatencyFirst: (json['cost_latency_first'] as num?)?.toInt(), + pathLatencyLatencyFirst: + (json['path_latency_latency_first'] as num?)?.toInt(), + ); + +Map _$RouteToJson(Route instance) => { + 'peer_id': instance.peerId, + 'ipv4_addr': instance.ipv4Addr, + 'next_hop_peer_id': instance.nextHopPeerId, + 'cost': instance.cost, + 'path_latency': instance.pathLatency, + 'proxy_cidrs': instance.proxyCidrs, + 'hostname': instance.hostname, + 'stun_info': instance.stunInfo, + 'inst_id': instance.instId, + 'version': instance.version, + 'feature_flag': instance.featureFlag, + 'next_hop_peer_id_latency_first': instance.nextHopPeerIdLatencyFirst, + 'cost_latency_first': instance.costLatencyFirst, + 'path_latency_latency_first': instance.pathLatencyLatencyFirst, + }; + +TunnelInfo _$TunnelInfoFromJson(Map json) => TunnelInfo( + tunnelType: json['tunnel_type'] as String, + localAddr: json['local_addr'] == null + ? null + : Url.fromJson(json['local_addr'] as Map), + remoteAddr: json['remote_addr'] == null + ? null + : Url.fromJson(json['remote_addr'] as Map), + ); + +Map _$TunnelInfoToJson(TunnelInfo instance) => + { + 'tunnel_type': instance.tunnelType, + 'local_addr': instance.localAddr, + 'remote_addr': instance.remoteAddr, + }; + +PeerConnStats _$PeerConnStatsFromJson(Map json) => + PeerConnStats( + rxBytes: (json['rx_bytes'] as num).toInt(), + txBytes: (json['tx_bytes'] as num).toInt(), + rxPackets: (json['rx_packets'] as num).toInt(), + txPackets: (json['tx_packets'] as num).toInt(), + latencyUs: (json['latency_us'] as num).toInt(), + ); + +Map _$PeerConnStatsToJson(PeerConnStats instance) => + { + 'rx_bytes': instance.rxBytes, + 'tx_bytes': instance.txBytes, + 'rx_packets': instance.rxPackets, + 'tx_packets': instance.txPackets, + 'latency_us': instance.latencyUs, + }; + +PeerConnInfo _$PeerConnInfoFromJson(Map json) => PeerConnInfo( + connId: json['conn_id'] as String, + myPeerId: (json['my_peer_id'] as num).toInt(), + peerId: (json['peer_id'] as num).toInt(), + features: + (json['features'] as List).map((e) => e as String).toList(), + tunnel: json['tunnel'] == null + ? null + : TunnelInfo.fromJson(json['tunnel'] as Map), + stats: json['stats'] == null + ? null + : PeerConnStats.fromJson(json['stats'] as Map), + lossRate: (json['loss_rate'] as num).toDouble(), + isClient: json['is_client'] as bool, + networkName: json['network_name'] as String, + ); + +Map _$PeerConnInfoToJson(PeerConnInfo instance) => + { + 'conn_id': instance.connId, + 'my_peer_id': instance.myPeerId, + 'peer_id': instance.peerId, + 'features': instance.features, + 'tunnel': instance.tunnel, + 'stats': instance.stats, + 'loss_rate': instance.lossRate, + 'is_client': instance.isClient, + 'network_name': instance.networkName, + }; + +PeerInfo _$PeerInfoFromJson(Map json) => PeerInfo( + peerId: (json['peer_id'] as num).toInt(), + conns: (json['conns'] as List) + .map((e) => PeerConnInfo.fromJson(e as Map)) + .toList(), + ); + +Map _$PeerInfoToJson(PeerInfo instance) => { + 'peer_id': instance.peerId, + 'conns': instance.conns, + }; + +PeerRoutePair _$PeerRoutePairFromJson(Map json) => + PeerRoutePair( + route: json['route'] == null + ? null + : Route.fromJson(json['route'] as Map), + peer: json['peer'] == null + ? null + : PeerInfo.fromJson(json['peer'] as Map), + ); + +Map _$PeerRoutePairToJson(PeerRoutePair instance) => + { + 'route': instance.route, + 'peer': instance.peer, + }; + +Ipv6Addr _$Ipv6AddrFromJson(Map json) => Ipv6Addr( + part1: (json['part1'] as num).toInt(), + part2: (json['part2'] as num).toInt(), + part3: (json['part3'] as num).toInt(), + part4: (json['part4'] as num).toInt(), + ); + +Map _$Ipv6AddrToJson(Ipv6Addr instance) => { + 'part1': instance.part1, + 'part2': instance.part2, + 'part3': instance.part3, + 'part4': instance.part4, + }; + +GetIpListResponse _$GetIpListResponseFromJson(Map json) => + GetIpListResponse( + publicIpv4: json['public_ipv4'] == null + ? null + : Ipv4Addr.fromJson(json['public_ipv4'] as Map), + interfaceIpv4s: (json['interface_ipv4s'] as List) + .map((e) => Ipv4Addr.fromJson(e as Map)) + .toList(), + publicIpv6: json['public_ipv6'] == null + ? null + : Ipv6Addr.fromJson(json['public_ipv6'] as Map), + interfaceIpv6s: (json['interface_ipv6s'] as List) + .map((e) => Ipv6Addr.fromJson(e as Map)) + .toList(), + listeners: (json['listeners'] as List) + .map((e) => Url.fromJson(e as Map)) + .toList(), + ); + +Map _$GetIpListResponseToJson(GetIpListResponse instance) => + { + 'public_ipv4': instance.publicIpv4, + 'interface_ipv4s': instance.interfaceIpv4s, + 'public_ipv6': instance.publicIpv6, + 'interface_ipv6s': instance.interfaceIpv6s, + 'listeners': instance.listeners, + }; + +MyNodeInfo _$MyNodeInfoFromJson(Map json) => MyNodeInfo( + virtualIpv4: json['virtual_ipv4'] == null + ? null + : Ipv4Inet.fromJson(json['virtual_ipv4'] as Map), + hostname: json['hostname'] as String, + version: json['version'] as String, + ips: json['ips'] == null + ? null + : GetIpListResponse.fromJson(json['ips'] as Map), + stunInfo: json['stun_info'] == null + ? null + : StunInfo.fromJson(json['stun_info'] as Map), + listeners: (json['listeners'] as List) + .map((e) => Url.fromJson(e as Map)) + .toList(), + vpnPortalCfg: json['vpn_portal_cfg'] as String?, + ); + +Map _$MyNodeInfoToJson(MyNodeInfo instance) => + { + 'virtual_ipv4': instance.virtualIpv4, + 'hostname': instance.hostname, + 'version': instance.version, + 'ips': instance.ips, + 'stun_info': instance.stunInfo, + 'listeners': instance.listeners, + 'vpn_portal_cfg': instance.vpnPortalCfg, + }; + +Runin _$RuninFromJson(Map json) => Runin( + devName: json['dev_name'] as String?, + myNodeInfo: json['my_node_info'] == null + ? null + : MyNodeInfo.fromJson(json['my_node_info'] as Map), + events: + (json['events'] as List).map((e) => e as String).toList(), + routes: (json['routes'] as List) + .map((e) => Route.fromJson(e as Map)) + .toList(), + peers: (json['peers'] as List?) + ?.map((e) => PeerInfo.fromJson(e as Map)) + .toList(), + peerRoutePairs: (json['peer_route_pairs'] as List?) + ?.map((e) => PeerRoutePair.fromJson(e as Map)) + .toList(), + running: json['running'] as bool, + errorMsg: json['error_msg'] as String?, + ); + +Map _$RuninToJson(Runin instance) => { + 'dev_name': instance.devName, + 'my_node_info': instance.myNodeInfo, + 'events': instance.events, + 'routes': instance.routes, + 'peers': instance.peers, + 'peer_route_pairs': instance.peerRoutePairs, + 'running': instance.running, + 'error_msg': instance.errorMsg, + }; diff --git a/lib/utils/主题工具.dart b/lib/utils/主题工具.dart new file mode 100644 index 0000000..a64e07e --- /dev/null +++ b/lib/utils/主题工具.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +String getThemeModeText(ThemeMode mode) { + return switch (mode) { + ThemeMode.light => '浅色模式', + ThemeMode.dark => '深色模式', + ThemeMode.system => '跟随系统', + }; +} diff --git a/lib/utils/状态.dart b/lib/utils/状态.dart new file mode 100644 index 0000000..167b9d2 --- /dev/null +++ b/lib/utils/状态.dart @@ -0,0 +1,64 @@ +import 'package:ASTRAL/src/rust/api/simple.dart'; +import 'package:flutter/foundation.dart'; +import '../config/app_config.dart'; + +class KM extends ChangeNotifier { + final _config = AppConfig(); + int _count = 0; + int get count => _count; + + void increment() { + _count++; + notifyListeners(); // 通知监听器重建UI + } + +// 房间名 + String get roomName => _config.roomName; + set roomName(String value) { + _config.setRoomName(value); + notifyListeners(); // 通知监听器重建UI + } + + //房间密码设置 + String get roomPassword => _config.roomPassword; + set roomPassword(String value) { + _config.setRoomPassword(value); + notifyListeners(); + } + + //用户名设置 + String get username => _config.username; + set username(String value) { + _config.setUsername(value); + notifyListeners(); + } + + //虚拟IP设置 + String get virtualIP => _config.virtualIP; + set virtualIP(String value) { + _config.setVirtualIP(value); + notifyListeners(); + } + + //动态获取IP设置 + bool get dynamicIP => _config.dynamicIP; + set dynamicIP(bool value) { + _config.setDynamicIP(value); + notifyListeners(); + } + + //当前服务器IP + String get serverIP => _config.currentServer; + set serverIP(String value) { + _config.setCurrentServer(value); + notifyListeners(); + } + + // 节点列表 + List _nodes = []; + List get nodes => _nodes; + set nodes(List value) { + _nodes = value; + notifyListeners(); + } +} diff --git a/lib/widgets/主题选择器.dart b/lib/widgets/主题选择器.dart new file mode 100644 index 0000000..0c82e8d --- /dev/null +++ b/lib/widgets/主题选择器.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; + +void showThemeColorPicker( + BuildContext context, Color seedColor, Function(Color) changeSeedColor) { + // 创建一个临时颜色变量,避免直接修改状态 + Color pickerColor = seedColor; + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('选择主题颜色'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 使用更简单的BlockPicker替代复杂的ColorPicker + BlockPicker( + pickerColor: pickerColor, + onColorChanged: (color) { + pickerColor = color; + }, + availableColors: const [ + Colors.red, + Colors.pink, + Colors.purple, + Colors.deepPurple, + Colors.indigo, + Colors.blue, + Colors.lightBlue, + Colors.cyan, + Colors.teal, + Colors.green, + Colors.lightGreen, + Colors.lime, + Colors.yellow, + Colors.amber, + Colors.orange, + Colors.deepOrange, + Colors.brown, + Colors.grey, + Colors.blueGrey, + ], + ), + const SizedBox(height: 10), + // 添加一个自定义颜色按钮 + ElevatedButton.icon( + icon: const Icon(Icons.color_lens), + label: const Text('自定义颜色'), + onPressed: () { + Navigator.of(context).pop(); + _showAdvancedColorPicker( + context, pickerColor, changeSeedColor); + }, + ), + ], + ), + ), + actions: [ + TextButton( + child: const Text('取消'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('确定'), + onPressed: () { + // 使用 Future.microtask 延迟颜色更改,避免在当前帧中触发重建 + Navigator.of(context).pop(); + changeSeedColor(pickerColor); + }, + ), + ], + ); + }, + ); +} + +void _showAdvancedColorPicker( + BuildContext context, Color initialColor, Function(Color) changeSeedColor) { + Color pickerColor = initialColor; + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('自定义颜色'), + content: SingleChildScrollView( + child: ColorPicker( + pickerColor: pickerColor, + onColorChanged: (color) { + pickerColor = color; + }, + pickerAreaHeightPercent: 0.8, + enableAlpha: false, + displayThumbColor: true, + paletteType: PaletteType.hsvWithHue, + pickerAreaBorderRadius: const BorderRadius.all(Radius.circular(10)), + labelTypes: const [ColorLabelType.rgb, ColorLabelType.hex], + ), + ), + actions: [ + TextButton( + child: const Text('取消'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('确定'), + onPressed: () { + // 使用 Future.microtask 延迟颜色更改,避免在当前帧中触发重建 + Navigator.of(context).pop(); + Future.microtask(() { + changeSeedColor(pickerColor); + }); + }, + ), + ], + ); + }, + ); +} diff --git a/lib/widgets/卡片.dart b/lib/widgets/卡片.dart new file mode 100644 index 0000000..4242276 --- /dev/null +++ b/lib/widgets/卡片.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +class FloatingCard extends StatefulWidget { + final ColorScheme colorScheme; + final Widget child; + final double elevation; + final EdgeInsetsGeometry padding; + final Duration duration; + final double hoverElevation; + final double? maxWidth; // 添加最大宽度参数 + final double? height; // 添加最大宽度参数 + final bool enable3DEffect; // 是否启用3D效果 + final double maxRotationDegree; // 最大旋转角度 + final bool enableTranslateEffect; // 是否启用偏移效果 + final double maxTranslateDistance; // 最大偏移距离 + final double zTranslation; // Z轴偏移距离 + final bool riseOnHover; // 控制悬浮时是升起还是降下 + + const FloatingCard({ + super.key, + required this.colorScheme, + required this.child, + this.elevation = 4, + this.padding = const EdgeInsets.all(16.0), + this.duration = const Duration(milliseconds: 200), + this.hoverElevation = 8, + this.maxWidth, + this.height, + this.enable3DEffect = false, // 默认启用3D效果 + this.maxRotationDegree = 10, // 默认最大旋转角度为10度 + this.enableTranslateEffect = false, // 默认启用偏移效果 + this.maxTranslateDistance = 0.1, // 默认最大偏移距离为5 + this.zTranslation = 10, // 默认Z轴偏移距离为20 + this.riseOnHover = true, // 默认悬浮时升起 + }); + + @override + State createState() => _FloatingCardState(); +} + +class _FloatingCardState extends State { + bool isHovered = false; + Offset mousePosition = Offset.zero; + final GlobalKey _cardKey = GlobalKey(); + + // 获取卡片的尺寸和位置 + Rect? _getCardRect() { + final RenderBox? renderBox = + _cardKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox == null) return null; + final position = renderBox.localToGlobal(Offset.zero); + return Rect.fromLTWH( + position.dx, position.dy, renderBox.size.width, renderBox.size.height); + } + + // 计算旋转角度 + (double, double) _calculateRotation() { + final rect = _getCardRect(); + if (rect == null) return (0, 0); + + // 计算鼠标相对于卡片中心的位置 + final centerX = rect.width / 2; + final centerY = rect.height / 2; + final deltaX = (mousePosition.dx - centerX) / centerX; + final deltaY = (mousePosition.dy - centerY) / centerY; + + // 计算旋转角度,鼠标在右侧时向左倾斜(Y轴正向旋转),鼠标在下方时向上倾斜(X轴负向旋转) + final rotateY = deltaX * widget.maxRotationDegree; + final rotateX = -deltaY * widget.maxRotationDegree; + + return (rotateX, rotateY); + } + + @override + Widget build(BuildContext context) { + // 根据是否启用3D效果计算旋转角度 + final (rotateX, rotateY) = + isHovered && widget.enable3DEffect ? _calculateRotation() : (0.0, 0.0); + + // 根据是否启用偏移效果计算偏移距离 + final translateX = isHovered && widget.enableTranslateEffect + ? rotateY * widget.maxTranslateDistance + : 0.0; + final translateY = isHovered && widget.enableTranslateEffect + ? rotateX * widget.maxTranslateDistance + : 0.0; + + // 根据riseOnHover决定Z轴偏移方向 + final zDirection = widget.riseOnHover ? 1.0 : -1.0; + final translateZ = isHovered && widget.enableTranslateEffect + ? widget.zTranslation * zDirection + : 0.0; + + return MouseRegion( + onEnter: (_) => setState(() => isHovered = true), + onExit: (_) => setState(() => isHovered = false), + onHover: (event) { + if (widget.enable3DEffect || widget.enableTranslateEffect) { + setState(() { + mousePosition = event.localPosition; + }); + } + }, + cursor: SystemMouseCursors.click, + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: widget.maxWidth ?? double.infinity, + ), + child: AnimatedContainer( + key: _cardKey, + duration: widget.duration, + transform: Matrix4.identity() + ..setEntry(3, 2, 0.001) // 透视效果 + // 仅当启用3D效果时应用旋转 + ..rotateX(widget.enable3DEffect ? rotateX * math.pi / 180 : 0) + ..rotateY(widget.enable3DEffect ? rotateY * math.pi / 180 : 0) + // 仅当启用偏移效果时应用偏移 + ..translate(translateX, translateY, translateZ), + transformAlignment: Alignment.center, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: isHovered ? widget.hoverElevation : widget.elevation, + child: InkWell( + borderRadius: BorderRadius.circular(12), + splashColor: widget.colorScheme.primary.withOpacity(0.1), + hoverColor: widget.colorScheme.primary.withOpacity(0.05), + onTap: () { + // 可以添加点击事件处理 + }, + child: Padding( + padding: widget.padding, + child: SizedBox( + height: widget.height, + child: widget.child, + )), + )), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/窗口控制按钮.dart b/lib/widgets/窗口控制按钮.dart new file mode 100644 index 0000000..23ba424 --- /dev/null +++ b/lib/widgets/窗口控制按钮.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:window_manager/window_manager.dart'; +import '../config/app_config.dart'; +import 'package:tray_manager/tray_manager.dart'; +class WindowControls extends StatelessWidget { + const WindowControls({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + IconButton( + icon: const Icon(Icons.remove), + onPressed: () => windowManager.minimize(), + tooltip: '最小化', + ), + IconButton( + icon: const Icon(Icons.crop_square), + onPressed: () async { + if (await windowManager.isMaximized()) { + windowManager.unmaximize(); + } else { + windowManager.maximize(); + } + }, + tooltip: '最大化/还原', + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + if (AppConfig().closeToTray) { + await windowManager.hide(); // 隐藏主窗口 + await trayManager.setToolTip('FLN2N 正在后台运行'); // 设置托盘提示 + } else { + windowManager.close(); + } + }, + tooltip: '关闭', +), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..414cbaa --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,997 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + url: "https://pub.flutter-io.cn" + source: hosted + version: "80.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.3.0" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.12.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.15" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.9.5" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.10.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.6" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.1" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.7" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.2" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: c1e26c7e48496be85104c16c040950b0436674cdf0737f3f6e95511b2529b592 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.63.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_colorpicker: + dependency: "direct main" + description: + name: flutter_colorpicker + sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_rust_bridge: + dependency: "direct main" + description: + name: flutter_rust_bridge + sha256: "5a5c7a5deeef2cc2ffe6076a33b0429f4a20ceac22a397297aed2b1eb067e611" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.0" + flutter_staggered_grid_view: + dependency: "direct main" + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + http: + dependency: transitive + description: + name: http + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.2" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: "81f04dee10969f89f604e1249382d46b97a1ccad53872875369622b5bfc9e58a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.9.4" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.0.8" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.11.1" + menu_base: + dependency: transitive + description: + name: menu_base + sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.3.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.3" + provider: + dependency: "direct main" + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" + rust_lib_fltier: + dependency: "direct main" + description: + path: rust_builder + relative: true + source: path + version: "0.0.1" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.8" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + shortid: + dependency: transitive + description: + name: shortid + sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.5" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.1" + system_tray: + dependency: "direct main" + description: + name: system_tray + sha256: "40444e5de8ed907822a98694fd031b8accc3cb3c0baa547634ce76189cf3d9cf" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.3" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.4" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + tray_manager: + dependency: "direct main" + description: + name: tray_manager + sha256: "80be6c508159a6f3c57983de795209ac13453e9832fd574143b06dceee188ed2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.15" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.flutter-io.cn" + source: hosted + version: "14.3.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.4" + win32: + dependency: transitive + description: + name: win32 + sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.12.0" + window_manager: + dependency: "direct main" + description: + name: window_manager + sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..d7a74ad --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,116 @@ +name: ASTRAL +description: "ASTRAL" +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.5.4 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + shared_preferences: ^2.5.2 + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + provider: ^6.0.5 + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + rust_lib_fltier: + path: rust_builder + flutter_rust_bridge: 2.9.0 + flutter_colorpicker: ^1.1.0 + window_manager: ^0.4.3 + fl_chart: ^0.63.0 + flutter_staggered_grid_view: ^0.7.0 + json_annotation: ^4.8.1 + tray_manager: ^0.3.2 + system_tray: ^2.0.3 + url_launcher: ^6.3.1 + package_info_plus: ^8.3.0 # 添加这一行 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.6 + json_serializable: ^6.7.1 + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^4.0.0 + integration_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + assets: + - assets/dlls/ + - assets/icon.ico + + fonts: + - family: MiSans + fonts: + - asset: assets/fonts/MiSans-Demibold.ttf + - asset: assets/fonts/MiSans-Regular.ttf + weight: 800 + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1 @@ +/target diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..db750bd --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,5111 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "allo-isolate" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449e356a4864c017286dbbec0e12767ea07efba29e3b7d984194c2a7ff3c4550" +dependencies = [ + "anyhow", + "atomic", + "backtrace", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_log-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" + +[[package]] +name = "android_logger" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f" +dependencies = [ + "android_log-sys", + "env_logger", + "log", + "once_cell", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "async-compression" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310c9bcae737a48ef5cdee3174184e6d548b292739ede61a1f955ef76a738861" +dependencies = [ + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "zstd 0.13.3", + "zstd-safe 7.2.3", +] + +[[package]] +name = "async-event" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1222afd3d2bce3995035054046a279ae7aa154d70d0766cea050073f3fd7ddf" +dependencies = [ + "loom", + "pin-project-lite", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "async-ringbuf" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3535a88714d7b0d7e5bb26aa474c574b5a7d4bfce5e76a035686c35b1bcd4491" +dependencies = [ + "futures", + "ringbuf", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "async-trait" +version = "0.1.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic-shim" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cd4b51d303cf3501c301e8125df442128d3c6d7c69f71b27833d253de47e77" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto_impl" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12882f59de5360c748c4cbf569a042d5fb0eb515f7bea9c1f470b47f6ffbd73" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.7.1", + "object", + "rustc-demangle", +] + +[[package]] +name = "base62" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10e52a7bcb1d6beebee21fb5053af9e3cbb7a7ed1a4909e534040e676437ab1f" +dependencies = [ + "rustversion", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97d56060ee67d285efb8001fec9d2a4c710c32efd2e14b5cbb5ba71930fc2d" + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.100", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "boringtun-easytier" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f09b4d1ada8affba260cb185bbdf6d5acff42f924dea1a17f938cf3e8fbe475" +dependencies = [ + "aead", + "atomic-shim", + "base64 0.13.1", + "blake2", + "chacha20poly1305", + "hex", + "hmac", + "ip_network", + "ip_network_table", + "libc", + "nix 0.25.1", + "parking_lot", + "rand_core 0.6.4", + "ring", + "tracing", + "untrusted", + "x25519-dalek", +] + +[[package]] +name = "bstr" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "build-target" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytecodec" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf4c9d0bbf32eea58d7c0f812058138ee8edaf0f2802b6d03561b504729a325" +dependencies = [ + "byteorder", + "trackable 0.2.24", +] + +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "c2rust-bitfields" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b43c3f07ab0ef604fa6f595aa46ec2f8a22172c975e186f6f5bf9829a3b72c41" +dependencies = [ + "c2rust-bitfields-derive", +] + +[[package]] +name = "c2rust-bitfields-derive" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3cbc102e2597c9744c8bd8c15915d554300601c91a079430d309816b0912545" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cidr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdf600c45bd958cf2945c445264471cca8b6c8e67bc87b71affd6d7e5682621" +dependencies = [ + "serde", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", + "unicase", + "unicode-width 0.2.0", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "codepage" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dart-sys" +version = "4.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57967e4b200d767d091b961d6ab42cc7d0cc14fe9e052e75d0d3cf9eb732d895" +dependencies = [ + "cc", +] + +[[package]] +name = "dashmap" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" +dependencies = [ + "cfg-if", + "num_cpus", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" + +[[package]] +name = "defmt" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f6162c53f659f65d00619fe31f14556a6e9f8752ccc4a41bd177ffcf3d6130" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d135dd939bad62d7490b0002602d35b358dce5fd9233a709d3c1ef467d4bde6" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "defmt-parser" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3983b127f13995e68c1e29071e5d115cd96f215ccb5e6812e3728cd6f92653b3" +dependencies = [ + "thiserror 2.0.12", +] + +[[package]] +name = "delegate-attr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "diatomic-waker" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28025fb55a9d815acf7b0877555f437254f373036eec6ed265116c7a5c0825e9" +dependencies = [ + "loom", + "waker-fn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "easytier" +version = "2.2.2" +dependencies = [ + "anyhow", + "async-compression", + "async-recursion", + "async-ringbuf", + "async-stream", + "async-trait", + "atomic-shim", + "auto_impl", + "base64 0.22.1", + "bitflags 2.9.0", + "boringtun-easytier", + "bytecodec", + "byteorder", + "bytes", + "chrono", + "cidr", + "clap", + "crossbeam", + "dashmap 6.1.0", + "easytier-rpc-build", + "encoding", + "futures", + "gethostname", + "git-version", + "globwalk", + "hickory-resolver", + "http", + "http_req", + "humansize 2.1.3", + "kcp-sys", + "machine-uid", + "mimalloc-rust", + "netlink-packet-core", + "netlink-packet-route", + "netlink-packet-utils", + "netlink-sys", + "network-interface", + "nix 0.29.0", + "once_cell", + "parking_lot", + "percent-encoding", + "petgraph", + "pin-project-lite", + "pnet", + "prost", + "prost-build", + "prost-reflect", + "prost-reflect-build", + "prost-types", + "rand 0.8.5", + "rcgen", + "regex", + "reqwest", + "ring", + "ringbuf", + "rust-i18n", + "rustls", + "serde", + "serde_json", + "service-manager", + "smoltcp", + "socket2", + "stun_codec", + "sys-locale", + "tabled", + "tachyonix", + "thiserror 1.0.69", + "time", + "timedmap", + "tokio", + "tokio-rustls", + "tokio-stream", + "tokio-util", + "tokio-websockets", + "toml", + "tonic-build", + "tracing", + "tracing-appender", + "tracing-subscriber", + "tun-easytier", + "url", + "uuid", + "wildmatch", + "windows 0.52.0", + "windows-service", + "winreg 0.52.0", + "zerocopy 0.7.35", + "zip", +] + +[[package]] +name = "easytier-rpc-build" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24829168c28f6a448f57d18116c255dcbd2b8c25e76dbc60f6cd16d68ad2cf07" +dependencies = [ + "heck 0.5.0", + "prost-build", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87b881ab2524b96a5ce932056c7482ba6152e2226fed3936b3e592adeb95ca6d" +dependencies = [ + "codepage", + "encoding_rs", + "windows-sys 0.52.0", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.5", +] + +[[package]] +name = "flutter_rust_bridge" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8c0dee6249225e815dcff3f3a39b98d9f66fdb3c392a432715b646bfa4da02" +dependencies = [ + "allo-isolate", + "android_logger", + "anyhow", + "build-target", + "bytemuck", + "byteorder", + "console_error_panic_hook", + "dart-sys", + "delegate-attr", + "flutter_rust_bridge_macros", + "futures", + "js-sys", + "lazy_static", + "log", + "oslog", + "portable-atomic", + "threadpool", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "flutter_rust_bridge_macros" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e88d604908d9eccb4ca9c26640ce41033165cbef041460e704ae28bd5208bce" +dependencies = [ + "hex", + "md-5", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30" +dependencies = [ + "rustix 0.38.44", + "windows-targets 0.52.6", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "git-version" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" +dependencies = [ + "git-version-macro", +] + +[[package]] +name = "git-version-macro" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "h2" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-proto" +version = "0.24.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.8.5", + "thiserror 1.0.69", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot", + "rand 0.8.5", + "resolv-conf", + "smallvec", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http_req" +version = "0.13.1" +source = "git+https://github.com/EasyTier/http_req.git#b10aa9fc0db3067cc3d2174683a87250b80a1ea9" +dependencies = [ + "base64 0.22.1", + "rand 0.8.5", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "unicase", + "webpki", + "webpki-roots", + "zeroize", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "humansize" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.3", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + +[[package]] +name = "ip_network_table" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0" +dependencies = [ + "ip_network", + "ip_network_table-deps-treebitmap", +] + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2", + "widestring", + "windows-sys 0.48.0", + "winreg 0.50.0", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kcp-sys" +version = "0.1.0" +source = "git+https://github.com/EasyTier/kcp-sys#0f0a0558391ba391c089806c23f369651f6c9eeb" +dependencies = [ + "anyhow", + "auto_impl", + "bindgen", + "bitflags 2.9.0", + "bytes", + "cc", + "dashmap 6.1.0", + "parking_lot", + "rand 0.8.5", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "zerocopy 0.7.35", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.0", + "libc", +] + +[[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "machine-uid" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4506fa0abb0a2ea93f5862f55973da0a662d2ad0e98f337a1c5aac657f0892" +dependencies = [ + "libc", + "winreg 0.52.0", +] + +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mimalloc-rust" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb726c8298efb4010b2c46d8050e4be36cf807b9d9e98cb112f830914fc9bbe" +dependencies = [ + "cty", + "mimalloc-rust-sys", +] + +[[package]] +name = "mimalloc-rust-sys" +version = "1.7.9-source" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6413e13241a9809f291568133eca6694572cf528c1a6175502d090adce5dd5db" +dependencies = [ + "cc", + "cty", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "netlink-packet-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483325d4bfef65699214858f097d504eb812c38ce7077d165f301ec406c3066e" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.69", +] + +[[package]] +name = "netlink-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" +dependencies = [ + "bytes", + "libc", + "log", +] + +[[package]] +name = "network-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433419f898328beca4f2c6c73a1b52540658d92b0a99f0269330457e0fd998d5" +dependencies = [ + "cc", + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "normpath" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "oslog" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8343ce955f18e7e68c0207dd0ea776ec453035685395ababd2ea651c569728b3" +dependencies = [ + "cc", + "dashmap 4.0.2", + "log", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "papergrid" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7419ad52a7de9b60d33e11085a0fe3df1fbd5926aa3f93d3dd53afbc9e86725" +dependencies = [ + "bytecount", + "fnv", + "unicode-width 0.1.11", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +dependencies = [ + "base64 0.22.1", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "pnet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d" +dependencies = [ + "ipnetwork", + "pnet_base", + "pnet_datalink", + "pnet_packet", + "pnet_sys", + "pnet_transport", +] + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", + "serde", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "serde", + "winapi", +] + +[[package]] +name = "pnet_macros" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.100", +] + +[[package]] +name = "pnet_macros_support" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab" +dependencies = [ + "pnet_base", +] + +[[package]] +name = "pnet_packet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f" +dependencies = [ + "glob", + "pnet_base", + "pnet_macros", + "pnet_macros_support", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pnet_transport" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf" +dependencies = [ + "libc", + "pnet_base", + "pnet_packet", + "pnet_sys", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.23", +] + +[[package]] +name = "prettyplease" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1ccf34da56fc294e7d4ccf69a85992b7dfb826b7cf57bac6a70bba3494cc08a" +dependencies = [ + "proc-macro2", + "syn 2.0.100", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck 0.5.0", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.100", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "prost-reflect" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5edd582b62f5cde844716e66d92565d7faf7ab1445c8cebce6e00fba83ddb2" +dependencies = [ + "once_cell", + "prost", + "prost-reflect-derive", + "prost-types", +] + +[[package]] +name = "prost-reflect-build" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e2537231d94dd2778920c2ada37dd9eb1ac0325bb3ee3ee651bd44c1134123" +dependencies = [ + "prost-build", + "prost-reflect", +] + +[[package]] +name = "prost-reflect-derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fce6b22f15cc8d8d400a2b98ad29202b33bd56c7d9ddd815bc803a807ecb65" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy 0.8.23", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.1", +] + +[[package]] +name = "rcgen" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48406db8ac1f3cbc7dcdb56ec355343817958a356ff430259bb07baf7607e1e1" +dependencies = [ + "pem", + "ring", + "time", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389a89e494bbc88bebf30e23da98742c843863a16a352647716116aa71fae80a" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ringbuf" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726bb493fe9cac765e8f96a144c3a8396bdf766dedad22e504b70b908dcbceb4" +dependencies = [ + "crossbeam-utils", + "portable-atomic", +] + +[[package]] +name = "rust-i18n" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71b3a6e1c6565b77c86d868eea3068b0eb39582510f9c78cfbd5c67bd36fda9b" +dependencies = [ + "globwalk", + "once_cell", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6180d8506af2b485ffc1eab7fc6d15678336a694f2b5efac5f2ca78c52928275" +dependencies = [ + "glob", + "once_cell", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "serde_yml", + "syn 2.0.100", +] + +[[package]] +name = "rust-i18n-support" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938f16094e2b09e893b1f85c9da251739a832d4272a5957217977da3a0713bb6" +dependencies = [ + "arc-swap", + "base62", + "globwalk", + "itertools 0.11.0", + "lazy_static", + "normpath", + "once_cell", + "proc-macro2", + "regex", + "serde", + "serde_json", + "serde_yml", + "siphasher", + "toml", + "triomphe", +] + +[[package]] +name = "rust_lib_fltier" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "dashmap 6.1.0", + "easytier", + "flutter_rust_bridge", + "humansize 1.1.1", + "lazy_static", + "once_cell", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.2", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yml" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" +dependencies = [ + "indexmap", + "itoa", + "libyml", + "memchr", + "ryu", + "serde", + "version_check", +] + +[[package]] +name = "service-manager" +version = "0.8.0" +source = "git+https://github.com/chipsenkbeil/service-manager-rs.git?branch=main#0294d3b9769c8ef7db8b4e831fb1c4f14b7d473b" +dependencies = [ + "cfg-if", + "dirs", + "encoding-utils", + "encoding_rs", + "plist", + "which", + "xml-rs", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "smoltcp" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "defmt", + "heapless", + "managed", +] + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "stun_codec" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feed9dafe0bda84f2b6ca3ce726b0a1f1ac2e8b63c6ecfb89b08b32313247b5b" +dependencies = [ + "bytecodec", + "byteorder", + "crc", + "hmac", + "md5", + "sha1", + "trackable 1.3.0", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tabled" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c9303ee60b9bedf722012ea29ae3711ba13a67c9b9ae28993838b63057cb1b" +dependencies = [ + "papergrid", + "tabled_derive", +] + +[[package]] +name = "tabled_derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0fb8bfdc709786c154e24a66777493fb63ae97e3036d914c8666774c477069" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tachyonix" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86c3eafa053bbcc63bb4bfc5eb26362a33ea0bc2e589f28bce00287d1c167d45" +dependencies = [ + "async-event", + "crossbeam-utils", + "diatomic-waker", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix 0.38.44", + "windows-sys 0.52.0", +] + +[[package]] +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +dependencies = [ + "rustix 1.0.2", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" + +[[package]] +name = "time-macros" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "timedmap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825f6c8a18bc36d56a62f66af7296385b628c9c5543a8663d4c217fc920bfefd" + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-websockets" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842e11addde61da7c37ef205cd625ebcd7b607076ea62e4698f06bfd5fd01a03" +dependencies = [ + "base64 0.22.1", + "bytes", + "fastrand", + "futures-core", + "futures-sink", + "http", + "httparse", + "ring", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tokio-util", + "webpki-roots", +] + +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trackable" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98abb9e7300b9ac902cc04920945a874c1973e08c310627cc4458c04b70dd32" +dependencies = [ + "trackable 1.3.0", + "trackable_derive", +] + +[[package]] +name = "trackable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" +dependencies = [ + "trackable_derive", +] + +[[package]] +name = "trackable_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "triomphe" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tun-easytier" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10dff0358b37ef593a74c9d2264a1df126e169d194878732a4f99ff7b01678bd" +dependencies = [ + "bytes", + "cfg-if", + "futures-core", + "ipnet", + "libc", + "libloading", + "log", + "nix 0.29.0", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "windows-sys 0.59.0", + "wintun", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" +dependencies = [ + "getrandom 0.3.1", + "rand 0.9.0", + "serde", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9521621447c21497fac206ffe6e9f642f977c4f82eeba9201055f64884d9cb01" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "wildmatch" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-service" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" +dependencies = [ + "bitflags 2.9.0", + "widestring", + "windows-sys 0.52.0", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wintun" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da99be64b5aa3de869c16977994314d0759a698d9a73ab0a5b1d52e2282033ae" +dependencies = [ + "c2rust-bitfields", + "libloading", + "log", + "thiserror 1.0.69", + "windows-sys 0.52.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "xml-rs" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive 0.8.23", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe 7.2.3", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-safe" +version = "7.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.14+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..84a95f6 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "rust_lib_fltier" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "staticlib"] + +[dependencies] +flutter_rust_bridge = "=2.9.0" +lazy_static = "1.4" +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +once_cell = "1.19.0" +dashmap = "6.1.0" +chrono = "0.4.38" +humansize = "1.1.1" +tokio = "1.39.2" +anyhow = "1.0.95" +easytier = { path = "./easytier" } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } diff --git a/rust/easytier/Cargo.lock b/rust/easytier/Cargo.lock new file mode 100644 index 0000000..5a72f3e --- /dev/null +++ b/rust/easytier/Cargo.lock @@ -0,0 +1,5274 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "async-compression" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cf008e5e1a9e9e22a7d3c9a4992e21a350290069e36d8fb72304ed17e8f2d2" +dependencies = [ + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "zstd 0.13.3", + "zstd-safe 7.2.3", +] + +[[package]] +name = "async-event" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1222afd3d2bce3995035054046a279ae7aa154d70d0766cea050073f3fd7ddf" +dependencies = [ + "loom", + "pin-project-lite", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "async-ringbuf" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3535a88714d7b0d7e5bb26aa474c574b5a7d4bfce5e76a035686c35b1bcd4491" +dependencies = [ + "futures", + "ringbuf", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "atomic-shim" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cd4b51d303cf3501c301e8125df442128d3c6d7c69f71b27833d253de47e77" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto_impl" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12882f59de5360c748c4cbf569a042d5fb0eb515f7bea9c1f470b47f6ffbd73" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base62" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10e52a7bcb1d6beebee21fb5053af9e3cbb7a7ed1a4909e534040e676437ab1f" +dependencies = [ + "rustversion", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.100", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "boringtun-easytier" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f09b4d1ada8affba260cb185bbdf6d5acff42f924dea1a17f938cf3e8fbe475" +dependencies = [ + "aead", + "atomic-shim", + "base64 0.13.1", + "blake2", + "chacha20poly1305", + "hex", + "hmac", + "ip_network", + "ip_network_table", + "libc", + "nix 0.25.1", + "parking_lot", + "rand_core 0.6.4", + "ring", + "tracing", + "untrusted", + "x25519-dalek", +] + +[[package]] +name = "bstr" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytecodec" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf4c9d0bbf32eea58d7c0f812058138ee8edaf0f2802b6d03561b504729a325" +dependencies = [ + "byteorder", + "trackable 0.2.24", +] + +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "c2rust-bitfields" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b43c3f07ab0ef604fa6f595aa46ec2f8a22172c975e186f6f5bf9829a3b72c41" +dependencies = [ + "c2rust-bitfields-derive", +] + +[[package]] +name = "c2rust-bitfields-derive" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3cbc102e2597c9744c8bd8c15915d554300601c91a079430d309816b0912545" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cidr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdf600c45bd958cf2945c445264471cca8b6c8e67bc87b71affd6d7e5682621" +dependencies = [ + "serde", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", + "unicase", + "unicode-width 0.2.0", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "codepage" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" + +[[package]] +name = "defguard_wireguard_rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba16f17698d4b389907310af018b0c3a80b025bba9c38d947cbc6dd70921743" +dependencies = [ + "base64 0.21.7", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-generic", + "netlink-packet-route 0.17.1", + "netlink-packet-utils", + "netlink-packet-wireguard", + "netlink-sys", + "nix 0.27.1", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "defmt" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f6162c53f659f65d00619fe31f14556a6e9f8752ccc4a41bd177ffcf3d6130" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d135dd939bad62d7490b0002602d35b358dce5fd9233a709d3c1ef467d4bde6" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "defmt-parser" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3983b127f13995e68c1e29071e5d115cd96f215ccb5e6812e3728cd6f92653b3" +dependencies = [ + "thiserror 2.0.12", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "diatomic-waker" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28025fb55a9d815acf7b0877555f437254f373036eec6ed265116c7a5c0825e9" +dependencies = [ + "loom", + "waker-fn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "easytier" +version = "2.2.2" +dependencies = [ + "aes-gcm", + "anyhow", + "async-compression", + "async-recursion", + "async-ringbuf", + "async-stream", + "async-trait", + "atomic-shim", + "auto_impl", + "base64 0.22.1", + "bitflags 2.9.0", + "boringtun-easytier", + "bytecodec", + "byteorder", + "bytes", + "chrono", + "cidr", + "clap", + "crossbeam", + "dashmap", + "defguard_wireguard_rs", + "easytier-rpc-build", + "encoding", + "futures", + "futures-util", + "gethostname", + "git-version", + "globwalk", + "hickory-resolver", + "http", + "http_req", + "humansize", + "kcp-sys", + "machine-uid", + "mimalloc-rust", + "netlink-packet-core", + "netlink-packet-route 0.21.0", + "netlink-packet-utils", + "netlink-sys", + "network-interface", + "nix 0.29.0", + "once_cell", + "parking_lot", + "percent-encoding", + "petgraph", + "pin-project-lite", + "pnet", + "prost", + "prost-build", + "prost-reflect", + "prost-reflect-build", + "prost-types", + "quinn", + "rand 0.8.5", + "rcgen", + "regex", + "reqwest", + "ring", + "ringbuf", + "rstest", + "rust-i18n", + "rustls", + "serde", + "serde_json", + "serial_test", + "service-manager", + "smoltcp", + "socket2", + "stun_codec", + "sys-locale", + "tabled", + "tachyonix", + "thiserror 1.0.69", + "time", + "timedmap", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-stream", + "tokio-util", + "tokio-websockets", + "toml", + "tonic-build", + "tracing", + "tracing-appender", + "tracing-subscriber", + "tun-easytier", + "url", + "uuid", + "wildmatch", + "windows 0.52.0", + "windows-service", + "winreg 0.52.0", + "zerocopy 0.7.35", + "zip", +] + +[[package]] +name = "easytier-rpc-build" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24829168c28f6a448f57d18116c255dcbd2b8c25e76dbc60f6cd16d68ad2cf07" +dependencies = [ + "heck 0.5.0", + "prost-build", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87b881ab2524b96a5ce932056c7482ba6152e2226fed3936b3e592adeb95ca6d" +dependencies = [ + "codepage", + "encoding_rs", + "windows-sys 0.52.0", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30" +dependencies = [ + "rustix 0.38.44", + "windows-targets 0.52.6", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "git-version" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" +dependencies = [ + "git-version-macro", +] + +[[package]] +name = "git-version-macro" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "h2" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-proto" +version = "0.24.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.8.5", + "thiserror 1.0.69", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot", + "rand 0.8.5", + "resolv-conf", + "smallvec", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows 0.52.0", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http_req" +version = "0.13.1" +source = "git+https://github.com/EasyTier/http_req.git#b10aa9fc0db3067cc3d2174683a87250b80a1ea9" +dependencies = [ + "base64 0.22.1", + "rand 0.8.5", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "unicase", + "webpki", + "webpki-roots", + "zeroize", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.9", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + +[[package]] +name = "ip_network_table" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0" +dependencies = [ + "ip_network", + "ip_network_table-deps-treebitmap", +] + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2", + "widestring", + "windows-sys 0.48.0", + "winreg 0.50.0", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kcp-sys" +version = "0.1.0" +source = "git+https://github.com/EasyTier/kcp-sys#0f0a0558391ba391c089806c23f369651f6c9eeb" +dependencies = [ + "anyhow", + "auto_impl", + "bindgen", + "bitflags 2.9.0", + "bytes", + "cc", + "dashmap", + "parking_lot", + "rand 0.8.5", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "zerocopy 0.7.35", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.0", + "libc", +] + +[[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "machine-uid" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4506fa0abb0a2ea93f5862f55973da0a662d2ad0e98f337a1c5aac657f0892" +dependencies = [ + "libc", + "winreg 0.52.0", +] + +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mimalloc-rust" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb726c8298efb4010b2c46d8050e4be36cf807b9d9e98cb112f830914fc9bbe" +dependencies = [ + "cty", + "mimalloc-rust-sys", +] + +[[package]] +name = "mimalloc-rust-sys" +version = "1.7.9-source" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6413e13241a9809f291568133eca6694572cf528c1a6175502d090adce5dd5db" +dependencies = [ + "cc", + "cty", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "netlink-packet-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-generic" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd7eb8ad331c84c6b8cb7f685b448133e5ad82e1ffd5acafac374af4a5a308b" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483325d4bfef65699214858f097d504eb812c38ce7077d165f301ec406c3066e" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.69", +] + +[[package]] +name = "netlink-packet-wireguard" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b25b050ff1f6a1e23c6777b72db22790fe5b6b5ccfd3858672587a79876c8f" +dependencies = [ + "anyhow", + "byteorder", + "libc", + "log", + "netlink-packet-generic", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" +dependencies = [ + "bytes", + "libc", + "log", +] + +[[package]] +name = "network-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433419f898328beca4f2c6c73a1b52540658d92b0a99f0269330457e0fd998d5" +dependencies = [ + "cc", + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "normpath" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "papergrid" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7419ad52a7de9b60d33e11085a0fe3df1fbd5926aa3f93d3dd53afbc9e86725" +dependencies = [ + "bytecount", + "fnv", + "unicode-width 0.1.11", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +dependencies = [ + "base64 0.22.1", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "pnet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d" +dependencies = [ + "ipnetwork", + "pnet_base", + "pnet_datalink", + "pnet_packet", + "pnet_sys", + "pnet_transport", +] + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", + "serde", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "serde", + "winapi", +] + +[[package]] +name = "pnet_macros" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.100", +] + +[[package]] +name = "pnet_macros_support" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab" +dependencies = [ + "pnet_base", +] + +[[package]] +name = "pnet_packet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f" +dependencies = [ + "glob", + "pnet_base", + "pnet_macros", + "pnet_macros_support", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pnet_transport" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf" +dependencies = [ + "libc", + "pnet_base", + "pnet_packet", + "pnet_sys", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.23", +] + +[[package]] +name = "prettyplease" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +dependencies = [ + "proc-macro2", + "syn 2.0.100", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck 0.5.0", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.100", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "prost-reflect" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5edd582b62f5cde844716e66d92565d7faf7ab1445c8cebce6e00fba83ddb2" +dependencies = [ + "once_cell", + "prost", + "prost-reflect-derive", + "prost-types", +] + +[[package]] +name = "prost-reflect-build" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e2537231d94dd2778920c2ada37dd9eb1ac0325bb3ee3ee651bd44c1134123" +dependencies = [ + "prost-build", + "prost-reflect", +] + +[[package]] +name = "prost-reflect-derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fce6b22f15cc8d8d400a2b98ad29202b33bd56c7d9ddd815bc803a807ecb65" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom 0.2.15", + "rand 0.8.5", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy 0.8.23", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.1", +] + +[[package]] +name = "rcgen" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48406db8ac1f3cbc7dcdb56ec355343817958a356ff430259bb07baf7607e1e1" +dependencies = [ + "pem", + "ring", + "time", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "reqwest" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e327e510263980e231de548a33e63d34962d29ae61b467389a1a09627a254" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "resolv-conf" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48375394603e3dd4b2d64371f7148fd8c7baa2680e28741f2cb8d23b59e3d4c4" +dependencies = [ + "hostname", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ringbuf" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726bb493fe9cac765e8f96a144c3a8396bdf766dedad22e504b70b908dcbceb4" +dependencies = [ + "crossbeam-utils", + "portable-atomic", +] + +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.100", + "unicode-ident", +] + +[[package]] +name = "rust-i18n" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71b3a6e1c6565b77c86d868eea3068b0eb39582510f9c78cfbd5c67bd36fda9b" +dependencies = [ + "globwalk", + "once_cell", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6180d8506af2b485ffc1eab7fc6d15678336a694f2b5efac5f2ca78c52928275" +dependencies = [ + "glob", + "once_cell", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "serde_yml", + "syn 2.0.100", +] + +[[package]] +name = "rust-i18n-support" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938f16094e2b09e893b1f85c9da251739a832d4272a5957217977da3a0713bb6" +dependencies = [ + "arc-swap", + "base62", + "globwalk", + "itertools 0.11.0", + "lazy_static", + "normpath", + "once_cell", + "proc-macro2", + "regex", + "serde", + "serde_json", + "serde_yml", + "siphasher", + "toml", + "triomphe", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.3", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c7dc240fec5517e6c4eab3310438636cfe6391dfc345ba013109909a90d136" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scc" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea091f6cac2595aa38993f04f4ee692ed43757035c36e67c180b6828356385b1" +dependencies = [ + "sdd", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "core-foundation-sys", + "libc", + "num-bigint", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yml" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" +dependencies = [ + "indexmap", + "itoa", + "libyml", + "memchr", + "ryu", + "serde", + "version_check", +] + +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "service-manager" +version = "0.8.0" +source = "git+https://github.com/chipsenkbeil/service-manager-rs.git?branch=main#0294d3b9769c8ef7db8b4e831fb1c4f14b7d473b" +dependencies = [ + "cfg-if", + "dirs", + "encoding-utils", + "encoding_rs", + "plist", + "which", + "xml-rs", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "smoltcp" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "defmt", + "heapless", + "managed", +] + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "stun_codec" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feed9dafe0bda84f2b6ca3ce726b0a1f1ac2e8b63c6ecfb89b08b32313247b5b" +dependencies = [ + "bytecodec", + "byteorder", + "crc", + "hmac", + "md5", + "sha1", + "trackable 1.3.0", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tabled" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c9303ee60b9bedf722012ea29ae3711ba13a67c9b9ae28993838b63057cb1b" +dependencies = [ + "papergrid", + "tabled_derive", +] + +[[package]] +name = "tabled_derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0fb8bfdc709786c154e24a66777493fb63ae97e3036d914c8666774c477069" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tachyonix" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86c3eafa053bbcc63bb4bfc5eb26362a33ea0bc2e589f28bce00287d1c167d45" +dependencies = [ + "async-event", + "crossbeam-utils", + "diatomic-waker", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "tempfile" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" +dependencies = [ + "fastrand", + "getrandom 0.3.1", + "once_cell", + "rustix 1.0.2", + "windows-sys 0.59.0", +] + +[[package]] +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +dependencies = [ + "rustix 1.0.2", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" + +[[package]] +name = "time-macros" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "timedmap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825f6c8a18bc36d56a62f66af7296385b628c9c5543a8663d4c217fc920bfefd" + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-websockets" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842e11addde61da7c37ef205cd625ebcd7b607076ea62e4698f06bfd5fd01a03" +dependencies = [ + "base64 0.22.1", + "bytes", + "fastrand", + "futures-core", + "futures-sink", + "http", + "httparse", + "ring", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tokio-util", + "webpki-roots", +] + +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trackable" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98abb9e7300b9ac902cc04920945a874c1973e08c310627cc4458c04b70dd32" +dependencies = [ + "trackable 1.3.0", + "trackable_derive", +] + +[[package]] +name = "trackable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" +dependencies = [ + "trackable_derive", +] + +[[package]] +name = "trackable_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "triomphe" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tun-easytier" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10dff0358b37ef593a74c9d2264a1df126e169d194878732a4f99ff7b01678bd" +dependencies = [ + "bytes", + "cfg-if", + "futures-core", + "ipnet", + "libc", + "libloading", + "log", + "nix 0.29.0", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "windows-sys 0.59.0", + "wintun", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.1", + "rand 0.9.0", + "serde", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dcd78c4f979627a754f5522cea6e6a25e55139056535fe6e69c506cd64a862" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-root-certs" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "wildmatch" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-service" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" +dependencies = [ + "bitflags 2.9.0", + "widestring", + "windows-sys 0.52.0", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wintun" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da99be64b5aa3de869c16977994314d0759a698d9a73ab0a5b1d52e2282033ae" +dependencies = [ + "c2rust-bitfields", + "libloading", + "log", + "thiserror 1.0.69", + "windows-sys 0.52.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "xml-rs" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive 0.8.23", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe 7.2.3", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-safe" +version = "7.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.14+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/rust/easytier/Cargo.toml b/rust/easytier/Cargo.toml new file mode 100644 index 0000000..78ba3ab --- /dev/null +++ b/rust/easytier/Cargo.toml @@ -0,0 +1,267 @@ +[package] +name = "easytier" +description = "A full meshed p2p VPN, connecting all your devices in one network with one command." +homepage = "https://github.com/EasyTier/EasyTier" +repository = "https://github.com/EasyTier/EasyTier" +version = "2.2.2" +edition = "2021" +authors = ["kkrainbow"] +keywords = ["vpn", "p2p", "network", "easytier"] +categories = ["network-programming", "command-line-utilities"] +rust-version = "1.84.0" +license-file = "LICENSE" +readme = "README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "easytier-core" +path = "src/easytier-core.rs" +test = false + +[[bin]] +name = "easytier-cli" +path = "src/easytier-cli.rs" + +[lib] +name = "easytier" +path = "src/lib.rs" + +[dependencies] +git-version = "0.3.9" + +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", features = [ + "env-filter", + "local-time", + "time", +] } +tracing-appender = "0.2.3" +thiserror = "1.0" +auto_impl = "1.1.0" +crossbeam = "0.8.4" +time = "0.3" +toml = "0.8.12" +chrono = { version = "0.4.37", features = ["serde"] } + +gethostname = "0.5.0" + +futures = { version = "0.3", features = ["bilock", "unstable"] } + +tokio = { version = "1", features = ["full"] } +tokio-stream = "0.1" +tokio-util = { version = "0.7.9", features = ["codec", "net", "io"] } + +async-stream = "0.3.5" +async-trait = "0.1.74" + +dashmap = "6.0" +timedmap = "=1.0.1" + +# for full-path zero-copy +zerocopy = { version = "0.7.32", features = ["derive", "simd"] } +bytes = "1.5.0" +pin-project-lite = "0.2.13" +tachyonix = "0.3.0" + +quinn = { version = "0.11.0", optional = true, features = ["ring"] } +rustls = { version = "0.23.0", features = [ + "ring", +], default-features = false, optional = true } +rcgen = { version = "0.12.1", optional = true } + +# for websocket +tokio-websockets = { version = "0.8", optional = true, features = [ + "rustls-webpki-roots", + "client", + "server", + "fastrand", + "ring", +] } +http = { version = "1", default-features = false, features = [ + "std", +], optional = true } +tokio-rustls = { version = "0.26", default-features = false, optional = true } + +# for tap device +tun = { package = "tun-easytier", version = "1.1.1", features = [ + "async", +], optional = true } +# for net ns +nix = { version = "0.29.0", features = ["sched", "socket", "ioctl", "net"] } + +uuid = { version = "1.5.0", features = [ + "v4", + "fast-rng", + "macro-diagnostics", + "serde", +] } + +# for ring tunnel +once_cell = "1.18.0" + +# for rpc +prost = "0.13" +prost-types = "0.13" +anyhow = "1.0" + +url = { version = "2.5", features = ["serde"] } +percent-encoding = "2.3.1" + +# for tun packet +byteorder = "1.5.0" + +# for proxy +cidr = { version = "0.2.2", features = ["serde"] } +socket2 = "0.5.5" + +# for hole punching +stun_codec = "0.3.4" +bytecodec = "0.4.15" +rand = "0.8.5" + +serde = { version = "1.0", features = ["derive"] } +pnet = { version = "0.35.0", features = ["serde"] } +serde_json = "1" + +clap = { version = "4.5.30", features = [ + "string", + "unicode", + "derive", + "wrap_help", +] } + +async-recursion = "1.0.5" + +network-interface = "2.0" + +# for ospf route +petgraph = "0.7.1" + +# for wireguard +boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true } + +# for encryption +ring = { version = "0.17", optional = true } +bitflags = "2.5" +aes-gcm = { version = "0.10.3", optional = true } + +# for cli +tabled = "0.16" +humansize = "2.1.3" + +base64 = "0.22" + +mimalloc-rust = { version = "0.2.1", optional = true } + +# mips +atomic-shim = "0.2.0" + +smoltcp = { version = "0.12.0", optional = true, default-features = false, features = [ + "std", + "medium-ip", + "proto-ipv4", + "proto-ipv6", + "socket-tcp", + "socket-tcp-cubic", + "async", +] } +parking_lot = { version = "0.12.0", optional = true } + +wildmatch = "2.3.4" + +rust-i18n = "3" +sys-locale = "0.3" + +ringbuf = "0.4.5" +async-ringbuf = "0.3.1" + +service-manager = {git = "https://github.com/chipsenkbeil/service-manager-rs.git", branch = "main"} + +async-compression = { version = "0.4.17", default-features = false, features = ["zstd", "tokio"] } + +kcp-sys = { git = "https://github.com/EasyTier/kcp-sys" } + +prost-reflect = { version = "0.14.5", default-features = false, features = [ + "derive", +] } + +# for http connector +http_req = { git = "https://github.com/EasyTier/http_req.git", default-features = false, features = ["rust-tls"] } + +# for dns connector +hickory-resolver = "0.24.4" + +[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies] +machine-uid = "0.5.3" + +[target.'cfg(any(target_os = "linux"))'.dependencies] +netlink-sys = "0.8.7" +netlink-packet-route = "0.21.0" +netlink-packet-core = { version = "0.7.0" } +netlink-packet-utils = "0.5.2" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.52.0", features = [ + "Win32_Foundation", + "Win32_NetworkManagement_WindowsFirewall", + "Win32_System_Com", + "Win32_Networking", + "Win32_System_Ole", + "Win32_Networking_WinSock", + "Win32_System_IO", +]} +encoding = "0.2" +winreg = "0.52" +windows-service = "0.7.0" + +[build-dependencies] +tonic-build = "0.12" +globwalk = "0.8.1" +regex = "1" +prost-build = "0.13.2" +rpc_build = { package = "easytier-rpc-build", version = "0.1.0", features = ["internal-namespace"] } +prost-reflect-build = { version = "0.14.0" } + +[target.'cfg(windows)'.build-dependencies] +reqwest = { version = "0.12.12", features = ["blocking"] } +zip = "0.6.6" + + +[dev-dependencies] +serial_test = "3.0.0" +rstest = "0.18.2" +futures-util = "0.3.30" + +[target.'cfg(target_os = "linux")'.dev-dependencies] +defguard_wireguard_rs = "0.4.2" +tokio-socks = "0.5.2" + + +[features] +default = ["wireguard", "mimalloc", "websocket", "smoltcp", "tun", "socks5"] +full = [ + "quic", + "websocket", + "wireguard", + "mimalloc", + "aes-gcm", + "smoltcp", + "tun", + "socks5", +] +mips = ["aes-gcm", "mimalloc", "wireguard", "tun", "smoltcp", "socks5"] +wireguard = ["dep:boringtun", "dep:ring"] +quic = ["dep:quinn", "dep:rustls", "dep:rcgen"] +mimalloc = ["dep:mimalloc-rust"] +aes-gcm = ["dep:aes-gcm"] +tun = ["dep:tun"] +websocket = [ + "dep:tokio-websockets", + "dep:http", + "dep:tokio-rustls", + "dep:rustls", + "dep:rcgen", +] +smoltcp = ["dep:smoltcp", "dep:parking_lot"] +socks5 = ["dep:smoltcp"] diff --git a/rust/easytier/build.rs b/rust/easytier/build.rs new file mode 100644 index 0000000..39e0621 --- /dev/null +++ b/rust/easytier/build.rs @@ -0,0 +1,171 @@ +#[cfg(target_os = "windows")] +use std::{env, io::Cursor, path::PathBuf}; + +#[cfg(target_os = "windows")] +struct WindowsBuild {} + +#[cfg(target_os = "windows")] +impl WindowsBuild { + fn check_protoc_exist() -> Option { + let path = env::var_os("PROTOC").map(PathBuf::from); + if path.is_some() && path.as_ref().unwrap().exists() { + return path; + } + + let path = env::var_os("PATH").unwrap_or_default(); + for p in env::split_paths(&path) { + let p = p.join("protoc.exe"); + if p.exists() && p.is_file() { + return Some(p); + } + } + + None + } + + fn get_cargo_target_dir() -> Result> { + let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?); + let profile = std::env::var("PROFILE")?; + let mut target_dir = None; + let mut sub_path = out_dir.as_path(); + while let Some(parent) = sub_path.parent() { + if parent.ends_with(&profile) { + target_dir = Some(parent); + break; + } + sub_path = parent; + } + let target_dir = target_dir.ok_or("not found")?; + Ok(target_dir.to_path_buf()) + } + + fn download_protoc() -> PathBuf { + println!("cargo:info=use exist protoc: {:?}", "k"); + let out_dir = Self::get_cargo_target_dir().unwrap().join("protobuf"); + let fname = out_dir.join("bin/protoc.exe"); + if fname.exists() { + println!("cargo:info=use exist protoc: {:?}", fname); + return fname; + } + + println!("cargo:info=need download protoc, please wait..."); + + let url = "https://github.com/protocolbuffers/protobuf/releases/download/v26.0-rc1/protoc-26.0-rc-1-win64.zip"; + let response = reqwest::blocking::get(url).unwrap(); + println!("{:?}", response); + let mut content = response + .bytes() + .map(|v| v.to_vec()) + .map(Cursor::new) + .map(zip::ZipArchive::new) + .unwrap() + .unwrap(); + content.extract(out_dir).unwrap(); + + fname + } + + pub fn check_for_win() { + // add third_party dir to link search path + let target = std::env::var("TARGET").unwrap_or_default(); + + if target.contains("x86_64") { + println!("cargo:rustc-link-search=native=easytier/third_party/"); + } else if target.contains("aarch64") { + println!("cargo:rustc-link-search=native=easytier/third_party/arm64/"); + } + + let protoc_path = if let Some(o) = Self::check_protoc_exist() { + println!("cargo:info=use os exist protoc: {:?}", o); + o + } else { + Self::download_protoc() + }; + std::env::set_var("PROTOC", protoc_path); + } +} + +fn workdir() -> Option { + if let Ok(cargo_manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { + return Some(cargo_manifest_dir); + } + + let dest = std::env::var("OUT_DIR"); + if dest.is_err() { + return None; + } + let dest = dest.unwrap(); + + let seperator = regex::Regex::new(r"(/target/(.+?)/build/)|(\\target\\(.+?)\\build\\)") + .expect("Invalid regex"); + let parts = seperator.split(dest.as_str()).collect::>(); + + if parts.len() >= 2 { + return Some(parts[0].to_string()); + } + + None +} + +fn check_locale() { + let workdir = workdir().unwrap_or("./".to_string()); + + let locale_path = format!("{workdir}/**/locales/**/*"); + if let Ok(globs) = globwalk::glob(locale_path) { + for entry in globs { + if let Err(e) = entry { + println!("cargo:i18n-error={}", e); + continue; + } + + let entry = entry.unwrap().into_path(); + println!("cargo:rerun-if-changed={}", entry.display()); + } + } +} + +fn main() -> Result<(), Box> { + #[cfg(target_os = "windows")] + WindowsBuild::check_for_win(); + + let proto_files_reflect = ["src/proto/peer_rpc.proto", "src/proto/common.proto"]; + + let proto_files = [ + "src/proto/error.proto", + "src/proto/tests.proto", + "src/proto/cli.proto", + "src/proto/web.proto", + ]; + + for proto_file in proto_files.iter().chain(proto_files_reflect.iter()) { + println!("cargo:rerun-if-changed={}", proto_file); + } + + let mut config = prost_build::Config::new(); + config + .protoc_arg("--experimental_allow_proto3_optional") + .type_attribute(".common", "#[derive(serde::Serialize, serde::Deserialize)]") + .type_attribute(".error", "#[derive(serde::Serialize, serde::Deserialize)]") + .type_attribute(".cli", "#[derive(serde::Serialize, serde::Deserialize)]") + .type_attribute(".web", "#[derive(serde::Serialize, serde::Deserialize)]") + .type_attribute( + "peer_rpc.GetIpListResponse", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + .type_attribute("peer_rpc.DirectConnectedPeerInfo", "#[derive(Hash)]") + .type_attribute("peer_rpc.PeerInfoForGlobalMap", "#[derive(Hash)]") + .type_attribute("peer_rpc.ForeignNetworkRouteInfoKey", "#[derive(Hash, Eq)]") + .type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]") + .field_attribute(".web.NetworkConfig", "#[serde(default)]") + .service_generator(Box::new(rpc_build::ServiceGenerator::new())) + .btree_map(["."]); + + config.compile_protos(&proto_files, &["src/proto/"])?; + + prost_reflect_build::Builder::new() + .file_descriptor_set_bytes("crate::proto::DESCRIPTOR_POOL_BYTES") + .compile_protos_with_config(config, &proto_files_reflect, &["src/proto/"])?; + + check_locale(); + Ok(()) +} diff --git a/rust/easytier/locales/app.yml b/rust/easytier/locales/app.yml new file mode 100644 index 0000000..d681204 --- /dev/null +++ b/rust/easytier/locales/app.yml @@ -0,0 +1,156 @@ +_version: 2 + +core_clap: + config_server: + en: |+ + config server address, allow format: + full url: --config-server udp://127.0.0.1:22020/admin + only user name: --config-server admin, will use official server + zh-CN: |+ + 配置服务器地址。允许格式: + 完整URL:--config-server udp://127.0.0.1:22020/admin + 仅用户名:--config-server admin,将使用官方的服务器 + config_file: + en: "path to the config file, NOTE: if this is set, all other options will be ignored" + zh-CN: "配置文件路径,注意:如果设置了这个选项,其他所有选项都将被忽略" + network_name: + en: "network name to identify this vpn network" + zh-CN: "用于标识此VPN网络的网络名称" + network_secret: + en: "network secret to verify this node belongs to the vpn network" + zh-CN: "网络密钥,用于验证此节点属于VPN网络" + ipv4: + en: "ipv4 address of this vpn node, if empty, this node will only forward packets and no TUN device will be created" + zh-CN: "此VPN节点的IPv4地址,如果为空,则此节点将仅转发数据包,不会创建TUN设备" + dhcp: + en: "automatically determine and set IP address by Easytier, and the IP address starts from 10.0.0.1 by default. Warning, if there is an IP conflict in the network when using DHCP, the IP will be automatically changed." + zh-CN: "由Easytier自动确定并设置IP地址,默认从10.0.0.1开始。警告:在使用DHCP时,如果网络中出现IP冲突,IP将自动更改。" + peers: + en: "peers to connect initially" + zh-CN: "最初要连接的对等节点" + external_node: + en: "use a public shared node to discover peers" + zh-CN: "使用公共共享节点来发现对等节点" + proxy_networks: + en: "export local networks to other peers in the vpn" + zh-CN: "将本地网络导出到VPN中的其他对等节点" + rpc_portal: + en: "rpc portal address to listen for management. 0 means random port, 12345 means listen on 12345 of localhost, 0.0.0.0:12345 means listen on 12345 of all interfaces. default is 0 and will try 15888 first" + zh-CN: "用于管理的RPC门户地址。0表示随机端口,12345表示在localhost的12345上监听,0.0.0.0:12345表示在所有接口的12345上监听。默认是0,首先尝试15888" + listeners: + en: |+ + listeners to accept connections, allow format: + port number: <11010>. means tcp/udp will listen on 11010, ws/wss will listen on 11010 and 11011, wg will listen on 11011 + url: . tcp can be tcp, udp, ring, wg, ws, wss\n + proto & port pair: . wg:11011, means listen on 11011 with wireguard protocol url and proto:port can occur multiple times. + zh-CN: |+ + 监听器用于接受连接,允许以下格式: + 端口号:<11010>,意味着tcp/udp将在11010端口监听,ws/wss将在11010和11011端口监听,wg将在11011端口监听。 + url:,其中tcp可以是tcp、udp、ring、wg、ws、wss协议。 + 协议和端口对:,例如wg:11011,表示使用WireGuard协议在11011端口监听。URL 和 协议端口对 可以多次出现。 + no_listener: + en: "do not listen on any port, only connect to peers" + zh-CN: "不监听任何端口,只连接到对等节点" + console_log_level: + en: "console log level" + zh-CN: "控制台日志级别" + file_log_level: + en: "file log level" + zh-CN: "文件日志级别" + file_log_dir: + en: "directory to store log files" + zh-CN: "存储日志文件的目录" + hostname: + en: "host name to identify this device" + zh-CN: "用于标识此设备的主机名" + instance_name: + en: "instance name to identify this vpn node in same machine" + zh-CN: "实例名称,用于在同一台机器上标识此VPN节点" + vpn_portal: + en: "url that defines the vpn portal, allow other vpn clients to connect. example: wg://0.0.0.0:11010/10.14.14.0/24, means the vpn portal is a wireguard server listening on vpn.example.com:11010, and the vpn client is in network of 10.14.14.0/24" + zh-CN: "定义VPN门户的URL,允许其他VPN客户端连接。示例:wg://0.0.0.0:11010/10.14.14.0/24,表示VPN门户是监听在vpn.example.com:11010的wireguard服务器,VPN客户端在10.14.14.0/24网络中" + default_protocol: + en: "default protocol to use when connecting to peers" + zh-CN: "连接到对等节点时使用的默认协议" + disable_encryption: + en: "disable encryption for peers communication, default is false, must be same with peers" + zh-CN: "禁用对等节点通信的加密,默认为false,必须与对等节点相同" + multi_thread: + en: "use multi-thread runtime, default is single-thread" + zh-CN: "使用多线程运行时,默认为单线程" + disable_ipv6: + en: "do not use ipv6" + zh-CN: "不使用IPv6" + dev_name: + en: "optional tun interface name" + zh-CN: "可选的TUN接口名称" + mtu: + en: "mtu of the TUN device, default is 1380 for non-encryption, 1360 for encryption" + zh-CN: "TUN设备的MTU,默认为非加密时为1380,加密时为1360" + latency_first: + en: "latency first mode, will try to relay traffic with lowest latency path, default is using shortest path" + zh-CN: "延迟优先模式,将尝试使用最低延迟路径转发流量,默认使用最短路径" + exit_nodes: + en: "exit nodes to forward all traffic to, a virtual ipv4 address, priority is determined by the order of the list" + zh-CN: "转发所有流量的出口节点,虚拟IPv4地址,优先级由列表顺序决定" + enable_exit_node: + en: "allow this node to be an exit node" + zh-CN: "允许此节点成为出口节点" + proxy_forward_by_system: + en: "forward packet to proxy networks via system kernel, disable internal nat for network proxy" + zh-CN: "通过系统内核转发子网代理数据包,禁用内置NAT" + no_tun: + en: "do not create TUN device, can use subnet proxy to access node" + zh-CN: "不创建TUN设备,可以使用子网代理访问节点" + use_smoltcp: + en: "enable smoltcp stack for subnet proxy and kcp proxy" + zh-CN: "为子网代理和 KCP 代理启用smoltcp堆栈" + manual_routes: + en: "assign routes cidr manually, will disable subnet proxy and wireguard routes propagated from peers. e.g.: 192.168.0.0/16" + zh-CN: "手动分配路由CIDR,将禁用子网代理和从对等节点传播的wireguard路由。例如:192.168.0.0/16" + relay_network_whitelist: + en: |+ + only forward traffic from the whitelist networks, supporting wildcard strings, multiple network names can be separated by spaces. + if local network (assigned with network_name) is not in the whitelist, the traffic can still be forwarded if no other route path is available. + if this parameter is empty, forwarding is disabled. by default, all networks are allowed. + e.g.: '*' (all networks), 'def*' (networks with the prefix 'def'), 'net1 net2' (only allow net1 and net2)" + zh-CN: |+ + 仅转发白名单网络的流量,支持通配符字符串。多个网络名称间可以使用英文空格间隔。 + 如果本地网络(使用 network_name 分配)不在白名单中,如果没有其他路由路径可用,流量仍然可以转发。 + 如果该参数为空,则禁用转发。默认允许所有网络。 + 例如:'*'(所有网络),'def*'(以def为前缀的网络),'net1 net2'(只允许net1和net2)" + disable_p2p: + en: "disable p2p communication, will only relay packets with peers specified by --peers" + zh-CN: "禁用P2P通信,只通过--peers指定的节点转发数据包" + disable_udp_hole_punching: + en: "disable udp hole punching" + zh-CN: "禁用UDP打洞功能" + relay_all_peer_rpc: + en: "relay all peer rpc packets, even if the peer is not in the relay network whitelist. this can help peers not in relay network whitelist to establish p2p connection." + zh-CN: "转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。这可以帮助白名单外网络中的对等节点建立P2P连接。" + socks5: + en: "enable socks5 server, allow socks5 client to access virtual network. format: , e.g.: 1080" + zh-CN: "启用 socks5 服务器,允许 socks5 客户端访问虚拟网络. 格式: <端口>,例如:1080" + ipv6_listener: + en: "the url of the ipv6 listener, e.g.: tcp://[::]:11010, if not set, will listen on random udp port" + zh-CN: "IPv6 监听器的URL,例如:tcp://[::]:11010,如果未设置,将在随机UDP端口上监听" + compression: + en: "compression algorithm to use, support none, zstd. default is none" + zh-CN: "要使用的压缩算法,支持 none、zstd。默认为 none" + mapped_listeners: + en: "manually specify the public address of the listener, other nodes can use this address to connect to this node. e.g.: tcp://123.123.123.123:11223, can specify multiple." + zh-CN: "手动指定监听器的公网地址,其他节点可以使用该地址连接到本节点。例如:tcp://123.123.123.123:11223,可以指定多个。" + bind_device: + en: "bind the connector socket to physical devices to avoid routing issues. e.g.: subnet proxy segment conflicts with a node's segment, after binding the physical device, it can communicate with the node normally." + zh-CN: "将连接器的套接字绑定到物理设备以避免路由问题。比如子网代理网段与某节点的网段冲突,绑定物理设备后可以与该节点正常通信。" + enable_kcp_proxy: + en: "proxy tcp streams with kcp, improving the latency and throughput on the network with udp packet loss." + zh-CN: "使用 KCP 代理 TCP 流,提高在 UDP 丢包网络上的延迟和吞吐量。" + disable_kcp_input: + en: "do not allow other nodes to use kcp to proxy tcp streams to this node. when a node with kcp proxy enabled accesses this node, the original tcp connection is preserved." + zh-CN: "不允许其他节点使用 KCP 代理 TCP 流到此节点。开启 KCP 代理的节点访问此节点时,依然使用原始 TCP 连接。" + +core_app: + panic_backtrace_save: + en: "backtrace saved to easytier-panic.log" + zh-CN: "回溯信息已保存到easytier-panic.log" diff --git a/rust/easytier/src/arch/mod.rs b/rust/easytier/src/arch/mod.rs new file mode 100644 index 0000000..581af77 --- /dev/null +++ b/rust/easytier/src/arch/mod.rs @@ -0,0 +1,2 @@ +#[cfg(target_os = "windows")] +pub mod windows; diff --git a/rust/easytier/src/arch/windows.rs b/rust/easytier/src/arch/windows.rs new file mode 100644 index 0000000..904de87 --- /dev/null +++ b/rust/easytier/src/arch/windows.rs @@ -0,0 +1,237 @@ +use std::{io, net::SocketAddr, os::windows::io::AsRawSocket}; + +use anyhow::Context; +use network_interface::NetworkInterfaceConfig; +use windows::{ + core::BSTR, + Win32::{ + Foundation::{BOOL, FALSE}, + NetworkManagement::WindowsFirewall::{ + INetFwPolicy2, INetFwRule, NET_FW_ACTION_ALLOW, NET_FW_PROFILE2_PRIVATE, + NET_FW_PROFILE2_PUBLIC, NET_FW_RULE_DIR_IN, NET_FW_RULE_DIR_OUT, + }, + Networking::WinSock::{ + htonl, setsockopt, WSAGetLastError, WSAIoctl, IPPROTO_IP, IPPROTO_IPV6, + IPV6_UNICAST_IF, IP_UNICAST_IF, SIO_UDP_CONNRESET, SOCKET, SOCKET_ERROR, + }, + System::Com::{ + CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_MULTITHREADED, + }, + }, +}; + +pub fn disable_connection_reset(socket: &S) -> io::Result<()> { + let handle = SOCKET(socket.as_raw_socket() as usize); + + unsafe { + // Ignoring UdpSocket's WSAECONNRESET error + // https://github.com/shadowsocks/shadowsocks-rust/issues/179 + // https://stackoverflow.com/questions/30749423/is-winsock-error-10054-wsaeconnreset-normal-with-udp-to-from-localhost + // + // This is because `UdpSocket::recv_from` may return WSAECONNRESET + // if you called `UdpSocket::send_to` a destination that is not existed (may be closed). + // + // It is not an error. Could be ignored completely. + // We have to ignore it here because it will crash the server. + + let mut bytes_returned: u32 = 0; + let enable: BOOL = FALSE; + + let ret = WSAIoctl( + handle, + SIO_UDP_CONNRESET, + Some(&enable as *const _ as *const std::ffi::c_void), + std::mem::size_of_val(&enable) as u32, + None, + 0, + &mut bytes_returned as *mut _, + None, + None, + ); + + if ret == SOCKET_ERROR { + let err_code = WSAGetLastError(); + return Err(std::io::Error::from_raw_os_error(err_code.0)); + } + } + + Ok(()) +} + +pub fn interface_count() -> io::Result { + let ifaces = network_interface::NetworkInterface::show().map_err(|e| { + io::Error::new( + io::ErrorKind::NotFound, + format!("Failed to get interfaces. error: {}", e), + ) + })?; + Ok(ifaces.len()) +} + +pub fn find_interface_index(iface_name: &str) -> io::Result { + let ifaces = network_interface::NetworkInterface::show().map_err(|e| { + io::Error::new( + io::ErrorKind::NotFound, + format!("Failed to get interfaces. {}, error: {}", iface_name, e), + ) + })?; + if let Some(iface) = ifaces.iter().find(|iface| iface.name == iface_name) { + return Ok(iface.index); + } + tracing::error!("Failed to find interface index for {}", iface_name); + Err(io::Error::new( + io::ErrorKind::NotFound, + format!("{}", iface_name), + )) +} + +pub fn set_ip_unicast_if( + socket: &S, + addr: &SocketAddr, + iface: &str, +) -> io::Result<()> { + let handle = SOCKET(socket.as_raw_socket() as usize); + + let if_index = find_interface_index(iface)?; + + unsafe { + // https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options + let ret = match addr { + SocketAddr::V4(..) => { + let if_index = htonl(if_index); + let if_index_bytes = if_index.to_ne_bytes(); + setsockopt(handle, IPPROTO_IP.0, IP_UNICAST_IF, Some(&if_index_bytes)) + } + SocketAddr::V6(..) => { + let if_index_bytes = if_index.to_ne_bytes(); + setsockopt( + handle, + IPPROTO_IPV6.0, + IPV6_UNICAST_IF, + Some(&if_index_bytes), + ) + } + }; + + if ret == SOCKET_ERROR { + let err = std::io::Error::from_raw_os_error(WSAGetLastError().0); + tracing::error!( + "set IP_UNICAST_IF / IPV6_UNICAST_IF interface: {}, index: {}, error: {}", + iface, + if_index, + err + ); + return Err(err); + } + } + + Ok(()) +} + +pub fn setup_socket_for_win( + socket: &S, + bind_addr: &SocketAddr, + bind_dev: Option, + is_udp: bool, +) -> io::Result<()> { + if is_udp { + disable_connection_reset(socket)?; + } + + if let Some(iface) = bind_dev { + set_ip_unicast_if(socket, bind_addr, iface.as_str())?; + } + + Ok(()) +} + +struct ComInitializer; + +impl ComInitializer { + fn new() -> windows::core::Result { + unsafe { CoInitializeEx(None, COINIT_MULTITHREADED)? }; + Ok(Self) + } +} + +impl Drop for ComInitializer { + fn drop(&mut self) { + unsafe { + CoUninitialize(); + } + } +} + +pub fn do_add_self_to_firewall_allowlist(inbound: bool) -> anyhow::Result<()> { + let _com = ComInitializer::new()?; + // 创建防火墙策略实例 + let policy: INetFwPolicy2 = unsafe { + CoCreateInstance( + &windows::Win32::NetworkManagement::WindowsFirewall::NetFwPolicy2, + None, + CLSCTX_ALL, + ) + }?; + + // 创建防火墙规则实例 + let rule: INetFwRule = unsafe { + CoCreateInstance( + &windows::Win32::NetworkManagement::WindowsFirewall::NetFwRule, + None, + CLSCTX_ALL, + ) + }?; + + // 设置规则属性 + let exe_path = std::env::current_exe() + .with_context(|| "Failed to get current executable path when adding firewall rule")? + .to_string_lossy() + .replace(r"\\?\", ""); + + let name = BSTR::from(format!( + "EasyTier {} ({})", + exe_path, + if inbound { "Inbound" } else { "Outbound" } + )); + let desc = BSTR::from("Allow EasyTier to do subnet proxy and kcp proxy"); + let app_path = BSTR::from(&exe_path); + + unsafe { + rule.SetName(&name)?; + rule.SetDescription(&desc)?; + rule.SetApplicationName(&app_path)?; + rule.SetAction(NET_FW_ACTION_ALLOW)?; + if inbound { + rule.SetDirection(NET_FW_RULE_DIR_IN)?; // 允许入站连接 + } else { + rule.SetDirection(NET_FW_RULE_DIR_OUT)?; // 允许出站连接 + } + rule.SetEnabled(windows::Win32::Foundation::VARIANT_TRUE)?; + rule.SetProfiles(NET_FW_PROFILE2_PRIVATE.0 | NET_FW_PROFILE2_PUBLIC.0)?; + rule.SetGrouping(&BSTR::from("EasyTier"))?; + + // 获取规则集合并添加新规则 + let rules = policy.Rules()?; + rules.Remove(&name)?; // 先删除同名规则 + rules.Add(&rule)?; + } + + Ok(()) +} + +pub fn add_self_to_firewall_allowlist() -> anyhow::Result<()> { + do_add_self_to_firewall_allowlist(true)?; + do_add_self_to_firewall_allowlist(false)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add_self_to_firewall_allowlist() { + let res = add_self_to_firewall_allowlist(); + assert!(res.is_ok()); + } +} diff --git a/rust/easytier/src/common/compressor.rs b/rust/easytier/src/common/compressor.rs new file mode 100644 index 0000000..f61662c --- /dev/null +++ b/rust/easytier/src/common/compressor.rs @@ -0,0 +1,191 @@ +use async_compression::tokio::write::{ZstdDecoder, ZstdEncoder}; +use tokio::io::AsyncWriteExt; + +use zerocopy::{AsBytes as _, FromBytes as _}; + +use crate::tunnel::packet_def::{CompressorAlgo, CompressorTail, ZCPacket, COMPRESSOR_TAIL_SIZE}; + +type Error = anyhow::Error; + +#[async_trait::async_trait] +pub trait Compressor { + async fn compress( + &self, + packet: &mut ZCPacket, + compress_algo: CompressorAlgo, + ) -> Result<(), Error>; + async fn decompress(&self, packet: &mut ZCPacket) -> Result<(), Error>; +} + +pub struct DefaultCompressor {} + +impl DefaultCompressor { + pub fn new() -> Self { + DefaultCompressor {} + } + + pub async fn compress_raw( + &self, + data: &[u8], + compress_algo: CompressorAlgo, + ) -> Result, Error> { + let buf = match compress_algo { + CompressorAlgo::ZstdDefault => { + let mut o = ZstdEncoder::new(Vec::new()); + o.write_all(data).await?; + o.shutdown().await?; + o.into_inner() + } + CompressorAlgo::None => data.to_vec(), + }; + + Ok(buf) + } + + pub async fn decompress_raw( + &self, + data: &[u8], + compress_algo: CompressorAlgo, + ) -> Result, Error> { + let buf = match compress_algo { + CompressorAlgo::ZstdDefault => { + let mut o = ZstdDecoder::new(Vec::new()); + o.write_all(data).await?; + o.shutdown().await?; + o.into_inner() + } + CompressorAlgo::None => data.to_vec(), + }; + + Ok(buf) + } +} + +#[async_trait::async_trait] +impl Compressor for DefaultCompressor { + async fn compress( + &self, + zc_packet: &mut ZCPacket, + compress_algo: CompressorAlgo, + ) -> Result<(), Error> { + if matches!(compress_algo, CompressorAlgo::None) { + return Ok(()); + } + + let pm_header = zc_packet.peer_manager_header().unwrap(); + if pm_header.is_compressed() { + return Ok(()); + } + + let tail = CompressorTail::new(compress_algo); + let buf = self + .compress_raw(zc_packet.payload(), compress_algo) + .await?; + + if buf.len() + COMPRESSOR_TAIL_SIZE > pm_header.len.get() as usize { + // Compressed data is larger than original data, don't compress + return Ok(()); + } + + zc_packet + .mut_peer_manager_header() + .unwrap() + .set_compressed(true); + + let payload_offset = zc_packet.payload_offset(); + zc_packet.mut_inner().truncate(payload_offset); + zc_packet.mut_inner().extend_from_slice(&buf); + zc_packet.mut_inner().extend_from_slice(tail.as_bytes()); + + Ok(()) + } + + async fn decompress(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> { + let pm_header = zc_packet.peer_manager_header().unwrap(); + if !pm_header.is_compressed() { + return Ok(()); + } + + let payload_len = zc_packet.payload().len(); + if payload_len < COMPRESSOR_TAIL_SIZE { + return Err(anyhow::anyhow!("Packet too short: {}", payload_len)); + } + + let text_len = payload_len - COMPRESSOR_TAIL_SIZE; + + let tail = CompressorTail::ref_from_suffix(zc_packet.payload()) + .unwrap() + .clone(); + + let algo = tail + .get_algo() + .ok_or(anyhow::anyhow!("Unknown algo: {:?}", tail))?; + + let buf = self + .decompress_raw(&zc_packet.payload()[..text_len], algo) + .await?; + + if buf.len() != pm_header.len.get() as usize { + anyhow::bail!( + "Decompressed length mismatch: decompressed len {} != pm header len {}", + buf.len(), + pm_header.len.get() + ); + } + + zc_packet + .mut_peer_manager_header() + .unwrap() + .set_compressed(false); + + let payload_offset = zc_packet.payload_offset(); + zc_packet.mut_inner().truncate(payload_offset); + zc_packet.mut_inner().extend_from_slice(&buf); + + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[tokio::test] + async fn test_compress() { + let text = b"12345670000000000000000000"; + let mut packet = ZCPacket::new_with_payload(text); + packet.fill_peer_manager_hdr(0, 0, 0); + + let compressor = DefaultCompressor {}; + + compressor + .compress(&mut packet, CompressorAlgo::ZstdDefault) + .await + .unwrap(); + assert_eq!(packet.peer_manager_header().unwrap().is_compressed(), true); + + compressor.decompress(&mut packet).await.unwrap(); + assert_eq!(packet.payload(), text); + assert_eq!(packet.peer_manager_header().unwrap().is_compressed(), false); + } + + #[tokio::test] + async fn test_short_text_compress() { + let text = b"1234"; + let mut packet = ZCPacket::new_with_payload(text); + packet.fill_peer_manager_hdr(0, 0, 0); + + let compressor = DefaultCompressor {}; + + // short text can't be compressed + compressor + .compress(&mut packet, CompressorAlgo::ZstdDefault) + .await + .unwrap(); + assert_eq!(packet.peer_manager_header().unwrap().is_compressed(), false); + + compressor.decompress(&mut packet).await.unwrap(); + assert_eq!(packet.payload(), text); + assert_eq!(packet.peer_manager_header().unwrap().is_compressed(), false); + } +} diff --git a/rust/easytier/src/common/config.rs b/rust/easytier/src/common/config.rs new file mode 100644 index 0000000..fb89a59 --- /dev/null +++ b/rust/easytier/src/common/config.rs @@ -0,0 +1,640 @@ +use std::{ + net::{Ipv4Addr, SocketAddr}, + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use anyhow::Context; +use serde::{Deserialize, Serialize}; + +use crate::{proto::common::CompressionAlgoPb, tunnel::generate_digest_from_str}; + +pub type Flags = crate::proto::common::FlagsInConfig; + +pub fn gen_default_flags() -> Flags { + Flags { + default_protocol: "tcp".to_string(), + dev_name: "".to_string(), + enable_encryption: true, + enable_ipv6: true, + mtu: 1380, + latency_first: false, + enable_exit_node: false, + proxy_forward_by_system: false, + no_tun: false, + use_smoltcp: false, + relay_network_whitelist: "*".to_string(), + disable_p2p: false, + relay_all_peer_rpc: false, + disable_udp_hole_punching: false, + ipv6_listener: "udp://[::]:0".to_string(), + multi_thread: true, + data_compress_algo: CompressionAlgoPb::None.into(), + bind_device: true, + enable_kcp_proxy: false, + disable_kcp_input: false, + disable_relay_kcp: true, + } +} + +#[auto_impl::auto_impl(Box, &)] +pub trait ConfigLoader: Send + Sync { + fn get_id(&self) -> uuid::Uuid; + fn set_id(&self, id: uuid::Uuid); + + fn get_hostname(&self) -> String; + fn set_hostname(&self, name: Option); + + fn get_inst_name(&self) -> String; + fn set_inst_name(&self, name: String); + + fn get_netns(&self) -> Option; + fn set_netns(&self, ns: Option); + + fn get_ipv4(&self) -> Option; + fn set_ipv4(&self, addr: Option); + + fn get_dhcp(&self) -> bool; + fn set_dhcp(&self, dhcp: bool); + + fn add_proxy_cidr(&self, cidr: cidr::IpCidr); + fn remove_proxy_cidr(&self, cidr: cidr::IpCidr); + fn get_proxy_cidrs(&self) -> Vec; + + fn get_network_identity(&self) -> NetworkIdentity; + fn set_network_identity(&self, identity: NetworkIdentity); + + fn get_listener_uris(&self) -> Vec; + + fn get_file_logger_config(&self) -> FileLoggerConfig; + fn set_file_logger_config(&self, config: FileLoggerConfig); + fn get_console_logger_config(&self) -> ConsoleLoggerConfig; + fn set_console_logger_config(&self, config: ConsoleLoggerConfig); + + fn get_peers(&self) -> Vec; + fn set_peers(&self, peers: Vec); + + fn get_listeners(&self) -> Vec; + fn set_listeners(&self, listeners: Vec); + + fn get_mapped_listeners(&self) -> Vec; + fn set_mapped_listeners(&self, listeners: Option>); + + fn get_rpc_portal(&self) -> Option; + fn set_rpc_portal(&self, addr: SocketAddr); + + fn get_vpn_portal_config(&self) -> Option; + fn set_vpn_portal_config(&self, config: VpnPortalConfig); + + fn get_flags(&self) -> Flags; + fn set_flags(&self, flags: Flags); + + fn get_exit_nodes(&self) -> Vec; + fn set_exit_nodes(&self, nodes: Vec); + + fn get_routes(&self) -> Option>; + fn set_routes(&self, routes: Option>); + + fn get_socks5_portal(&self) -> Option; + fn set_socks5_portal(&self, addr: Option); + + fn dump(&self) -> String; +} + +pub type NetworkSecretDigest = [u8; 32]; + +#[derive(Debug, Clone, Deserialize, Serialize, Default, Eq, Hash)] +pub struct NetworkIdentity { + pub network_name: String, + pub network_secret: Option, + #[serde(skip)] + pub network_secret_digest: Option, +} + +impl PartialEq for NetworkIdentity { + fn eq(&self, other: &Self) -> bool { + if self.network_name != other.network_name { + return false; + } + + if self.network_secret.is_some() + && other.network_secret.is_some() + && self.network_secret != other.network_secret + { + return false; + } + + if self.network_secret_digest.is_some() + && other.network_secret_digest.is_some() + && self.network_secret_digest != other.network_secret_digest + { + return false; + } + + return true; + } +} + +impl NetworkIdentity { + pub fn new(network_name: String, network_secret: String) -> Self { + let mut network_secret_digest = [0u8; 32]; + generate_digest_from_str(&network_name, &network_secret, &mut network_secret_digest); + + NetworkIdentity { + network_name, + network_secret: Some(network_secret), + network_secret_digest: Some(network_secret_digest), + } + } + + pub fn default() -> Self { + Self::new("default".to_string(), "".to_string()) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct PeerConfig { + pub uri: url::Url, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct ProxyNetworkConfig { + pub cidr: String, + pub allow: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)] +pub struct FileLoggerConfig { + pub level: Option, + pub file: Option, + pub dir: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)] +pub struct ConsoleLoggerConfig { + pub level: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct VpnPortalConfig { + pub client_cidr: cidr::Ipv4Cidr, + pub wireguard_listen: SocketAddr, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +struct Config { + netns: Option, + hostname: Option, + instance_name: Option, + instance_id: Option, + ipv4: Option, + dhcp: Option, + network_identity: Option, + listeners: Option>, + mapped_listeners: Option>, + exit_nodes: Option>, + + peer: Option>, + proxy_network: Option>, + + file_logger: Option, + console_logger: Option, + + rpc_portal: Option, + + vpn_portal_config: Option, + + routes: Option>, + + socks5_proxy: Option, + + flags: Option>, + + #[serde(skip)] + flags_struct: Option, +} + +#[derive(Debug, Clone)] +pub struct TomlConfigLoader { + config: Arc>, +} + +impl Default for TomlConfigLoader { + fn default() -> Self { + TomlConfigLoader::new_from_str("").unwrap() + } +} + +impl TomlConfigLoader { + pub fn new_from_str(config_str: &str) -> Result { + let mut config = toml::de::from_str::(config_str) + .with_context(|| format!("failed to parse config file: {}", config_str))?; + + config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default())); + + Ok(TomlConfigLoader { + config: Arc::new(Mutex::new(config)), + }) + } + + pub fn new(config_path: &PathBuf) -> Result { + let config_str = std::fs::read_to_string(config_path) + .with_context(|| format!("failed to read config file: {:?}", config_path))?; + let ret = Self::new_from_str(&config_str)?; + let old_ns = ret.get_network_identity(); + ret.set_network_identity(NetworkIdentity::new( + old_ns.network_name, + old_ns.network_secret.unwrap_or_default(), + )); + + Ok(ret) + } + + fn gen_flags(mut flags_hashmap: serde_json::Map) -> Flags { + let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap(); + let default_flags_hashmap = + serde_json::from_str::>(&default_flags_json) + .unwrap(); + + let mut merged_hashmap = serde_json::Map::new(); + for (key, value) in default_flags_hashmap { + if let Some(v) = flags_hashmap.remove(&key) { + merged_hashmap.insert(key, v); + } else { + merged_hashmap.insert(key, value); + } + } + + serde_json::from_value(serde_json::Value::Object(merged_hashmap)).unwrap() + } +} + +impl ConfigLoader for TomlConfigLoader { + fn get_inst_name(&self) -> String { + self.config + .lock() + .unwrap() + .instance_name + .clone() + .unwrap_or("default".to_string()) + } + + fn set_inst_name(&self, name: String) { + self.config.lock().unwrap().instance_name = Some(name); + } + + fn get_hostname(&self) -> String { + let hostname = self.config.lock().unwrap().hostname.clone(); + + match hostname { + Some(hostname) => { + let hostname = hostname + .chars() + .filter(|c| !c.is_control()) + .take(32) + .collect::(); + + if !hostname.is_empty() { + self.set_hostname(Some(hostname.clone())); + hostname + } else { + self.set_hostname(None); + gethostname::gethostname().to_string_lossy().to_string() + } + } + None => gethostname::gethostname().to_string_lossy().to_string(), + } + } + + fn set_hostname(&self, name: Option) { + self.config.lock().unwrap().hostname = name; + } + + fn get_netns(&self) -> Option { + self.config.lock().unwrap().netns.clone() + } + + fn set_netns(&self, ns: Option) { + self.config.lock().unwrap().netns = ns; + } + + fn get_ipv4(&self) -> Option { + let locked_config = self.config.lock().unwrap(); + locked_config + .ipv4 + .as_ref() + .map(|s| s.parse().ok()) + .flatten() + .map(|c: cidr::Ipv4Inet| { + if c.network_length() == 32 { + cidr::Ipv4Inet::new(c.address(), 24).unwrap() + } else { + c + } + }) + } + + fn set_ipv4(&self, addr: Option) { + self.config.lock().unwrap().ipv4 = if let Some(addr) = addr { + Some(addr.to_string()) + } else { + None + }; + } + + fn get_dhcp(&self) -> bool { + self.config.lock().unwrap().dhcp.unwrap_or_default() + } + + fn set_dhcp(&self, dhcp: bool) { + self.config.lock().unwrap().dhcp = Some(dhcp); + } + + fn add_proxy_cidr(&self, cidr: cidr::IpCidr) { + let mut locked_config = self.config.lock().unwrap(); + if locked_config.proxy_network.is_none() { + locked_config.proxy_network = Some(vec![]); + } + let cidr_str = cidr.to_string(); + // insert if no duplicate + if !locked_config + .proxy_network + .as_ref() + .unwrap() + .iter() + .any(|c| c.cidr == cidr_str) + { + locked_config + .proxy_network + .as_mut() + .unwrap() + .push(ProxyNetworkConfig { + cidr: cidr_str, + allow: None, + }); + } + } + + fn remove_proxy_cidr(&self, cidr: cidr::IpCidr) { + let mut locked_config = self.config.lock().unwrap(); + if let Some(proxy_cidrs) = &mut locked_config.proxy_network { + let cidr_str = cidr.to_string(); + proxy_cidrs.retain(|c| c.cidr != cidr_str); + } + } + + fn get_proxy_cidrs(&self) -> Vec { + self.config + .lock() + .unwrap() + .proxy_network + .as_ref() + .map(|v| { + v.iter() + .map(|c| c.cidr.parse().unwrap()) + .collect::>() + }) + .unwrap_or_default() + } + + fn get_id(&self) -> uuid::Uuid { + let mut locked_config = self.config.lock().unwrap(); + if locked_config.instance_id.is_none() { + let id = uuid::Uuid::new_v4(); + locked_config.instance_id = Some(id); + id + } else { + locked_config.instance_id.as_ref().unwrap().clone() + } + } + + fn set_id(&self, id: uuid::Uuid) { + self.config.lock().unwrap().instance_id = Some(id); + } + + fn get_network_identity(&self) -> NetworkIdentity { + self.config + .lock() + .unwrap() + .network_identity + .clone() + .unwrap_or_else(NetworkIdentity::default) + } + + fn set_network_identity(&self, identity: NetworkIdentity) { + self.config.lock().unwrap().network_identity = Some(identity); + } + + fn get_listener_uris(&self) -> Vec { + self.config + .lock() + .unwrap() + .listeners + .clone() + .unwrap_or_default() + } + + fn get_file_logger_config(&self) -> FileLoggerConfig { + self.config + .lock() + .unwrap() + .file_logger + .clone() + .unwrap_or_default() + } + + fn set_file_logger_config(&self, config: FileLoggerConfig) { + self.config.lock().unwrap().file_logger = Some(config); + } + + fn get_console_logger_config(&self) -> ConsoleLoggerConfig { + self.config + .lock() + .unwrap() + .console_logger + .clone() + .unwrap_or_default() + } + + fn set_console_logger_config(&self, config: ConsoleLoggerConfig) { + self.config.lock().unwrap().console_logger = Some(config); + } + + fn get_peers(&self) -> Vec { + self.config.lock().unwrap().peer.clone().unwrap_or_default() + } + + fn set_peers(&self, peers: Vec) { + self.config.lock().unwrap().peer = Some(peers); + } + + fn get_listeners(&self) -> Vec { + self.config + .lock() + .unwrap() + .listeners + .clone() + .unwrap_or_default() + } + + fn set_listeners(&self, listeners: Vec) { + self.config.lock().unwrap().listeners = Some(listeners); + } + + fn get_mapped_listeners(&self) -> Vec { + self.config + .lock() + .unwrap() + .mapped_listeners + .clone() + .unwrap_or_default() + } + + fn set_mapped_listeners(&self, listeners: Option>) { + self.config.lock().unwrap().mapped_listeners = listeners; + } + + fn get_rpc_portal(&self) -> Option { + self.config.lock().unwrap().rpc_portal + } + + fn set_rpc_portal(&self, addr: SocketAddr) { + self.config.lock().unwrap().rpc_portal = Some(addr); + } + + fn get_vpn_portal_config(&self) -> Option { + self.config.lock().unwrap().vpn_portal_config.clone() + } + fn set_vpn_portal_config(&self, config: VpnPortalConfig) { + self.config.lock().unwrap().vpn_portal_config = Some(config); + } + + fn get_flags(&self) -> Flags { + self.config + .lock() + .unwrap() + .flags_struct + .clone() + .unwrap_or_default() + } + + fn set_flags(&self, flags: Flags) { + self.config.lock().unwrap().flags_struct = Some(flags); + } + + fn get_exit_nodes(&self) -> Vec { + self.config + .lock() + .unwrap() + .exit_nodes + .clone() + .unwrap_or_default() + } + + fn set_exit_nodes(&self, nodes: Vec) { + self.config.lock().unwrap().exit_nodes = Some(nodes); + } + + fn dump(&self) -> String { + let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap(); + let default_flags_hashmap = + serde_json::from_str::>(&default_flags_json) + .unwrap(); + + let cur_flags_json = serde_json::to_string(&self.get_flags()).unwrap(); + let cur_flags_hashmap = + serde_json::from_str::>(&cur_flags_json) + .unwrap(); + + let mut flag_map: serde_json::Map = Default::default(); + for (key, value) in default_flags_hashmap { + if let Some(v) = cur_flags_hashmap.get(&key) { + if *v != value { + flag_map.insert(key, v.clone()); + } + } + } + + let mut config = self.config.lock().unwrap().clone(); + config.flags = Some(flag_map); + toml::to_string_pretty(&config).unwrap() + } + + fn get_routes(&self) -> Option> { + self.config.lock().unwrap().routes.clone() + } + + fn set_routes(&self, routes: Option>) { + self.config.lock().unwrap().routes = routes; + } + + fn get_socks5_portal(&self) -> Option { + self.config.lock().unwrap().socks5_proxy.clone() + } + + fn set_socks5_portal(&self, addr: Option) { + self.config.lock().unwrap().socks5_proxy = addr; + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[tokio::test] + async fn full_example_test() { + let config_str = r#" +instance_name = "default" +instance_id = "87ede5a2-9c3d-492d-9bbe-989b9d07e742" +ipv4 = "10.144.144.10" +listeners = [ "tcp://0.0.0.0:11010", "udp://0.0.0.0:11010" ] +routes = [ "192.168.0.0/16" ] + +[network_identity] +network_name = "default" +network_secret = "" + +[[peer]] +uri = "tcp://public.kkrainbow.top:11010" + +[[peer]] +uri = "udp://192.168.94.33:11010" + +[[proxy_network]] +cidr = "10.147.223.0/24" +allow = ["tcp", "udp", "icmp"] + +[[proxy_network]] +cidr = "10.1.1.0/24" +allow = ["tcp", "icmp"] + +[file_logger] +level = "info" +file = "easytier" +dir = "/tmp/easytier" + +[console_logger] +level = "warn" +"#; + let ret = TomlConfigLoader::new_from_str(config_str); + if let Err(e) = &ret { + println!("{}", e); + } else { + println!("{:?}", ret.as_ref().unwrap()); + } + assert!(ret.is_ok()); + + let ret = ret.unwrap(); + assert_eq!("10.144.144.10/24", ret.get_ipv4().unwrap().to_string()); + + assert_eq!( + vec!["tcp://0.0.0.0:11010", "udp://0.0.0.0:11010"], + ret.get_listener_uris() + .iter() + .map(|u| u.to_string()) + .collect::>() + ); + + println!("{}", ret.dump()); + } +} diff --git a/rust/easytier/src/common/constants.rs b/rust/easytier/src/common/constants.rs new file mode 100644 index 0000000..f8b7f78 --- /dev/null +++ b/rust/easytier/src/common/constants.rs @@ -0,0 +1,35 @@ +macro_rules! define_global_var { + ($name:ident, $type:ty, $init:expr) => { + pub static $name: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| tokio::sync::Mutex::new($init)); + }; +} + +#[macro_export] +macro_rules! use_global_var { + ($name:ident) => { + crate::common::constants::$name.lock().await.to_owned() + }; +} + +#[macro_export] +macro_rules! set_global_var { + ($name:ident, $val:expr) => { + *crate::common::constants::$name.lock().await = $val + }; +} + +define_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS, u64, 1000); + +define_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, u64, 10); + +pub const UDP_HOLE_PUNCH_CONNECTOR_SERVICE_ID: u32 = 2; + +pub const WIN_SERVICE_WORK_DIR_REG_KEY: &str = "SOFTWARE\\EasyTier\\Service\\WorkDir"; + +pub const EASYTIER_VERSION: &str = git_version::git_version!( + args = ["--abbrev=8", "--always", "--dirty=~"], + prefix = concat!(env!("CARGO_PKG_VERSION"), "-@astral "), + suffix = "", + fallback = env!("CARGO_PKG_VERSION") +); diff --git a/rust/easytier/src/common/defer.rs b/rust/easytier/src/common/defer.rs new file mode 100644 index 0000000..1132c9b --- /dev/null +++ b/rust/easytier/src/common/defer.rs @@ -0,0 +1,24 @@ +#[doc(hidden)] +pub struct Defer { + // internal struct used by defer! macro + func: Option, +} + +impl Defer { + pub fn new(func: F) -> Self { + Self { func: Some(func) } + } +} + +impl Drop for Defer { + fn drop(&mut self) { + self.func.take().map(|f| f()); + } +} + +#[macro_export] +macro_rules! defer { + ( $($tt:tt)* ) => { + let _deferred = $crate::common::defer::Defer::new(|| { $($tt)* }); + }; +} diff --git a/rust/easytier/src/common/error.rs b/rust/easytier/src/common/error.rs new file mode 100644 index 0000000..045d363 --- /dev/null +++ b/rust/easytier/src/common/error.rs @@ -0,0 +1,55 @@ +use std::{io, result}; + +use thiserror::Error; + +use crate::tunnel; + +use super::PeerId; + +#[derive(Error, Debug)] +pub enum Error { + #[error("io error")] + IOError(#[from] io::Error), + + #[cfg(feature = "tun")] + #[error("rust tun error {0}")] + TunError(#[from] tun::Error), + + #[error("tunnel error {0}")] + TunnelError(#[from] tunnel::TunnelError), + #[error("Peer has no conn, PeerId: {0}")] + PeerNoConnectionError(PeerId), + #[error("RouteError: {0:?}")] + RouteError(Option), + #[error("Not found")] + NotFound, + #[error("Invalid Url: {0}")] + InvalidUrl(String), + #[error("Shell Command error: {0}")] + ShellCommandError(String), + // #[error("Rpc listen error: {0}")] + // RpcListenError(String), + #[error("Rpc connect error: {0}")] + RpcConnectError(String), + #[error("Timeout error: {0}")] + Timeout(#[from] tokio::time::error::Elapsed), + #[error("url in blacklist")] + UrlInBlacklist, + #[error("unknown data store error")] + Unknown, + #[error("anyhow error: {0}")] + AnyhowError(#[from] anyhow::Error), + + #[error("wait resp error: {0}")] + WaitRespError(String), + + #[error("message decode error: {0}")] + MessageDecodeError(String), + + #[error("secret key error: {0}")] + SecretKeyError(String), +} + +pub type Result = result::Result; + +// impl From for std:: diff --git a/rust/easytier/src/common/global_ctx.rs b/rust/easytier/src/common/global_ctx.rs new file mode 100644 index 0000000..caa723b --- /dev/null +++ b/rust/easytier/src/common/global_ctx.rs @@ -0,0 +1,344 @@ +use std::collections::hash_map::DefaultHasher; +use std::{ + hash::Hasher, + sync::{Arc, Mutex}, +}; + +use crate::proto::cli::PeerConnInfo; +use crate::proto::common::PeerFeatureFlag; +use crossbeam::atomic::AtomicCell; + +use super::{ + config::{ConfigLoader, Flags}, + netns::NetNS, + network::IPCollector, + stun::{StunInfoCollector, StunInfoCollectorTrait}, + PeerId, +}; + +pub type NetworkIdentity = crate::common::config::NetworkIdentity; + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum GlobalCtxEvent { + TunDeviceReady(String), + TunDeviceError(String), + + PeerAdded(PeerId), + PeerRemoved(PeerId), + PeerConnAdded(PeerConnInfo), + PeerConnRemoved(PeerConnInfo), + + ListenerAdded(url::Url), + ListenerAddFailed(url::Url, String), // (url, error message) + ListenerAcceptFailed(url::Url, String), // (url, error message) + ConnectionAccepted(String, String), // (local url, remote url) + ConnectionError(String, String, String), // (local url, remote url, error message) + + Connecting(url::Url), + ConnectError(String, String, String), // (dst, ip version, error message) + + VpnPortalClientConnected(String, String), // (portal, client ip) + VpnPortalClientDisconnected(String, String), // (portal, client ip) + + DhcpIpv4Changed(Option, Option), // (old, new) + DhcpIpv4Conflicted(Option), +} + +pub type EventBus = tokio::sync::broadcast::Sender; +pub type EventBusSubscriber = tokio::sync::broadcast::Receiver; + +pub struct GlobalCtx { + pub inst_name: String, + pub id: uuid::Uuid, + pub config: Box, + pub net_ns: NetNS, + pub network: NetworkIdentity, + + event_bus: EventBus, + + cached_ipv4: AtomicCell>, + cached_proxy_cidrs: AtomicCell>>, + + ip_collector: Arc, + + hostname: String, + + stun_info_collection: Box, + + running_listeners: Mutex>, + + enable_exit_node: bool, + proxy_forward_by_system: bool, + no_tun: bool, + + feature_flags: AtomicCell, +} + +impl std::fmt::Debug for GlobalCtx { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GlobalCtx") + .field("inst_name", &self.inst_name) + .field("id", &self.id) + .field("net_ns", &self.net_ns.name()) + .field("event_bus", &"EventBus") + .field("ipv4", &self.cached_ipv4) + .finish() + } +} + +pub type ArcGlobalCtx = std::sync::Arc; + +impl GlobalCtx { + pub fn new(config_fs: impl ConfigLoader + 'static + Send + Sync) -> Self { + let id = config_fs.get_id(); + let network = config_fs.get_network_identity(); + let net_ns = NetNS::new(config_fs.get_netns()); + let hostname = config_fs.get_hostname(); + + let (event_bus, _) = tokio::sync::broadcast::channel(1024); + + let stun_info_collection = Arc::new(StunInfoCollector::new_with_default_servers()); + + let enable_exit_node = config_fs.get_flags().enable_exit_node; + let proxy_forward_by_system = config_fs.get_flags().proxy_forward_by_system; + let no_tun = config_fs.get_flags().no_tun; + + let mut feature_flags = PeerFeatureFlag::default(); + feature_flags.kcp_input = !config_fs.get_flags().disable_kcp_input; + feature_flags.no_relay_kcp = config_fs.get_flags().disable_relay_kcp; + + GlobalCtx { + inst_name: config_fs.get_inst_name(), + id, + config: Box::new(config_fs), + net_ns: net_ns.clone(), + network, + + event_bus, + cached_ipv4: AtomicCell::new(None), + cached_proxy_cidrs: AtomicCell::new(None), + + ip_collector: Arc::new(IPCollector::new(net_ns, stun_info_collection.clone())), + + hostname, + + stun_info_collection: Box::new(stun_info_collection), + + running_listeners: Mutex::new(Vec::new()), + + enable_exit_node, + proxy_forward_by_system, + no_tun, + + feature_flags: AtomicCell::new(feature_flags), + } + } + + pub fn subscribe(&self) -> EventBusSubscriber { + self.event_bus.subscribe() + } + + pub fn issue_event(&self, event: GlobalCtxEvent) { + if self.event_bus.receiver_count() != 0 { + self.event_bus.send(event).unwrap(); + } else { + tracing::warn!("No subscriber for event: {:?}", event); + } + } + + pub fn check_network_in_whitelist(&self, network_name: &str) -> Result<(), anyhow::Error> { + if self + .get_flags() + .relay_network_whitelist + .split(" ") + .map(wildmatch::WildMatch::new) + .any(|wl| wl.matches(network_name)) + { + Ok(()) + } else { + Err(anyhow::anyhow!("network {} not in whitelist", network_name).into()) + } + } + + pub fn get_ipv4(&self) -> Option { + if let Some(ret) = self.cached_ipv4.load() { + return Some(ret); + } + let addr = self.config.get_ipv4(); + self.cached_ipv4.store(addr.clone()); + return addr; + } + + pub fn set_ipv4(&self, addr: Option) { + self.config.set_ipv4(addr); + self.cached_ipv4.store(None); + } + + pub fn add_proxy_cidr(&self, cidr: cidr::IpCidr) -> Result<(), std::io::Error> { + self.config.add_proxy_cidr(cidr); + self.cached_proxy_cidrs.store(None); + Ok(()) + } + + pub fn remove_proxy_cidr(&self, cidr: cidr::IpCidr) -> Result<(), std::io::Error> { + self.config.remove_proxy_cidr(cidr); + self.cached_proxy_cidrs.store(None); + Ok(()) + } + + pub fn get_proxy_cidrs(&self) -> Vec { + if let Some(proxy_cidrs) = self.cached_proxy_cidrs.take() { + self.cached_proxy_cidrs.store(Some(proxy_cidrs.clone())); + return proxy_cidrs; + } + + let ret = self.config.get_proxy_cidrs(); + self.cached_proxy_cidrs.store(Some(ret.clone())); + ret + } + + pub fn get_id(&self) -> uuid::Uuid { + self.config.get_id() + } + + pub fn get_network_identity(&self) -> NetworkIdentity { + self.config.get_network_identity() + } + + pub fn get_network_name(&self) -> String { + self.get_network_identity().network_name + } + + pub fn get_ip_collector(&self) -> Arc { + self.ip_collector.clone() + } + + pub fn get_hostname(&self) -> String { + return self.hostname.clone(); + } + + pub fn get_stun_info_collector(&self) -> impl StunInfoCollectorTrait + '_ { + self.stun_info_collection.as_ref() + } + + pub fn replace_stun_info_collector(&self, collector: Box) { + // force replace the stun_info_collection without mut and drop the old one + let ptr = &self.stun_info_collection as *const Box; + let ptr = ptr as *mut Box; + unsafe { + std::ptr::drop_in_place(ptr); + #[allow(invalid_reference_casting)] + std::ptr::write(ptr, collector); + } + } + + pub fn get_running_listeners(&self) -> Vec { + self.running_listeners.lock().unwrap().clone() + } + + pub fn add_running_listener(&self, url: url::Url) { + let mut l = self.running_listeners.lock().unwrap(); + if !l.contains(&url) { + l.push(url); + } + } + + pub fn get_vpn_portal_cidr(&self) -> Option { + self.config.get_vpn_portal_config().map(|x| x.client_cidr) + } + + pub fn get_flags(&self) -> Flags { + self.config.get_flags() + } + + pub fn set_flags(&self, flags: Flags) { + self.config.set_flags(flags); + } + + pub fn get_128_key(&self) -> [u8; 16] { + let mut key = [0u8; 16]; + let secret = self + .config + .get_network_identity() + .network_secret + .unwrap_or_default(); + // fill key according to network secret + let mut hasher = DefaultHasher::new(); + hasher.write(secret.as_bytes()); + key[0..8].copy_from_slice(&hasher.finish().to_be_bytes()); + hasher.write(&key[0..8]); + key[8..16].copy_from_slice(&hasher.finish().to_be_bytes()); + hasher.write(&key[0..16]); + key + } + + pub fn enable_exit_node(&self) -> bool { + self.enable_exit_node + } + + pub fn proxy_forward_by_system(&self) -> bool { + self.proxy_forward_by_system + } + + pub fn no_tun(&self) -> bool { + self.no_tun + } + + pub fn get_feature_flags(&self) -> PeerFeatureFlag { + self.feature_flags.load() + } + + pub fn set_feature_flags(&self, flags: PeerFeatureFlag) { + self.feature_flags.store(flags); + } +} + +#[cfg(test)] +pub mod tests { + use crate::common::{config::TomlConfigLoader, new_peer_id}; + + use super::*; + + #[tokio::test] + async fn test_global_ctx() { + let config = TomlConfigLoader::default(); + let global_ctx = GlobalCtx::new(config); + + let mut subscriber = global_ctx.subscribe(); + let peer_id = new_peer_id(); + global_ctx.issue_event(GlobalCtxEvent::PeerAdded(peer_id.clone())); + global_ctx.issue_event(GlobalCtxEvent::PeerRemoved(peer_id.clone())); + global_ctx.issue_event(GlobalCtxEvent::PeerConnAdded(PeerConnInfo::default())); + global_ctx.issue_event(GlobalCtxEvent::PeerConnRemoved(PeerConnInfo::default())); + + assert_eq!( + subscriber.recv().await.unwrap(), + GlobalCtxEvent::PeerAdded(peer_id.clone()) + ); + assert_eq!( + subscriber.recv().await.unwrap(), + GlobalCtxEvent::PeerRemoved(peer_id.clone()) + ); + assert_eq!( + subscriber.recv().await.unwrap(), + GlobalCtxEvent::PeerConnAdded(PeerConnInfo::default()) + ); + assert_eq!( + subscriber.recv().await.unwrap(), + GlobalCtxEvent::PeerConnRemoved(PeerConnInfo::default()) + ); + } + + pub fn get_mock_global_ctx_with_network( + network_identy: Option, + ) -> ArcGlobalCtx { + let config_fs = TomlConfigLoader::default(); + config_fs.set_inst_name(format!("test_{}", config_fs.get_id())); + config_fs.set_network_identity(network_identy.unwrap_or(NetworkIdentity::default())); + std::sync::Arc::new(GlobalCtx::new(config_fs)) + } + + pub fn get_mock_global_ctx() -> ArcGlobalCtx { + get_mock_global_ctx_with_network(None) + } +} diff --git a/rust/easytier/src/common/ifcfg/darwin.rs b/rust/easytier/src/common/ifcfg/darwin.rs new file mode 100644 index 0000000..a496726 --- /dev/null +++ b/rust/easytier/src/common/ifcfg/darwin.rs @@ -0,0 +1,81 @@ +use std::net::Ipv4Addr; + +use async_trait::async_trait; + +use super::{cidr_to_subnet_mask, run_shell_cmd, Error, IfConfiguerTrait}; + +pub struct MacIfConfiger {} +#[async_trait] +impl IfConfiguerTrait for MacIfConfiger { + async fn add_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "route -n add {} -netmask {} -interface {} -hopcount 7", + address, + cidr_to_subnet_mask(cidr_prefix), + name + ) + .as_str(), + ) + .await + } + + async fn remove_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "route -n delete {} -netmask {} -interface {}", + address, + cidr_to_subnet_mask(cidr_prefix), + name + ) + .as_str(), + ) + .await + } + + async fn add_ipv4_ip( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "ifconfig {} {:?}/{:?} 10.8.8.8 up", + name, address, cidr_prefix, + ) + .as_str(), + ) + .await + } + + async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> { + run_shell_cmd(format!("ifconfig {} {}", name, if up { "up" } else { "down" }).as_str()) + .await + } + + async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> { + if ip.is_none() { + run_shell_cmd(format!("ifconfig {} inet delete", name).as_str()).await + } else { + run_shell_cmd( + format!("ifconfig {} inet {} delete", name, ip.unwrap().to_string()).as_str(), + ) + .await + } + } + + async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> { + run_shell_cmd(format!("ifconfig {} mtu {}", name, mtu).as_str()).await + } +} diff --git a/rust/easytier/src/common/ifcfg/mod.rs b/rust/easytier/src/common/ifcfg/mod.rs new file mode 100644 index 0000000..79a8ea2 --- /dev/null +++ b/rust/easytier/src/common/ifcfg/mod.rs @@ -0,0 +1,127 @@ +#[cfg(any(target_os = "macos", target_os = "freebsd"))] +mod darwin; +#[cfg(any(target_os = "linux"))] +mod netlink; +#[cfg(target_os = "windows")] +mod windows; + +mod route; + +use std::net::Ipv4Addr; + +use async_trait::async_trait; +use tokio::process::Command; + +use super::error::Error; + +#[async_trait] +pub trait IfConfiguerTrait: Send + Sync { + async fn add_ipv4_route( + &self, + _name: &str, + _address: Ipv4Addr, + _cidr_prefix: u8, + ) -> Result<(), Error> { + Ok(()) + } + async fn remove_ipv4_route( + &self, + _name: &str, + _address: Ipv4Addr, + _cidr_prefix: u8, + ) -> Result<(), Error> { + Ok(()) + } + async fn add_ipv4_ip( + &self, + _name: &str, + _address: Ipv4Addr, + _cidr_prefix: u8, + ) -> Result<(), Error> { + Ok(()) + } + async fn set_link_status(&self, _name: &str, _up: bool) -> Result<(), Error> { + Ok(()) + } + async fn remove_ip(&self, _name: &str, _ip: Option) -> Result<(), Error> { + Ok(()) + } + async fn wait_interface_show(&self, _name: &str) -> Result<(), Error> { + return Ok(()); + } + async fn set_mtu(&self, _name: &str, _mtu: u32) -> Result<(), Error> { + Ok(()) + } +} + +fn cidr_to_subnet_mask(prefix_length: u8) -> Ipv4Addr { + if prefix_length > 32 { + panic!("Invalid CIDR prefix length"); + } + + let subnet_mask: u32 = (!0u32) + .checked_shl(32 - u32::from(prefix_length)) + .unwrap_or(0); + Ipv4Addr::new( + ((subnet_mask >> 24) & 0xFF) as u8, + ((subnet_mask >> 16) & 0xFF) as u8, + ((subnet_mask >> 8) & 0xFF) as u8, + (subnet_mask & 0xFF) as u8, + ) +} + +async fn run_shell_cmd(cmd: &str) -> Result<(), Error> { + let cmd_out: std::process::Output; + let stdout: String; + let stderr: String; + #[cfg(target_os = "windows")] + { + const CREATE_NO_WINDOW: u32 = 0x08000000; + cmd_out = Command::new("cmd") + .stdin(std::process::Stdio::null()) + .arg("/C") + .arg(cmd) + .creation_flags(CREATE_NO_WINDOW) + .output() + .await?; + stdout = crate::utils::utf8_or_gbk_to_string(cmd_out.stdout.as_slice()); + stderr = crate::utils::utf8_or_gbk_to_string(cmd_out.stderr.as_slice()); + }; + + #[cfg(not(target_os = "windows"))] + { + cmd_out = Command::new("sh").arg("-c").arg(cmd).output().await?; + stdout = String::from_utf8_lossy(cmd_out.stdout.as_slice()).to_string(); + stderr = String::from_utf8_lossy(cmd_out.stderr.as_slice()).to_string(); + }; + + let ec = cmd_out.status.code(); + let succ = cmd_out.status.success(); + tracing::info!(?cmd, ?ec, ?succ, ?stdout, ?stderr, "run shell cmd"); + + if !cmd_out.status.success() { + return Err(Error::ShellCommandError(stdout + &stderr)); + } + Ok(()) +} + +pub struct DummyIfConfiger {} +#[async_trait] +impl IfConfiguerTrait for DummyIfConfiger {} + +#[cfg(any(target_os = "linux"))] +pub type IfConfiger = netlink::NetlinkIfConfiger; + +#[cfg(any(target_os = "macos", target_os = "freebsd"))] +pub type IfConfiger = darwin::MacIfConfiger; + +#[cfg(target_os = "windows")] +pub type IfConfiger = windows::WindowsIfConfiger; + +#[cfg(not(any( + target_os = "macos", + target_os = "linux", + target_os = "windows", + target_os = "freebsd", +)))] +pub type IfConfiger = DummyIfConfiger; diff --git a/rust/easytier/src/common/ifcfg/netlink.rs b/rust/easytier/src/common/ifcfg/netlink.rs new file mode 100644 index 0000000..0ddcb2a --- /dev/null +++ b/rust/easytier/src/common/ifcfg/netlink.rs @@ -0,0 +1,577 @@ +use std::{ + ffi::CString, + fmt::Debug, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + num::NonZero, + os::fd::AsRawFd, +}; + +use anyhow::Context; +use async_trait::async_trait; +use cidr::IpInet; +use netlink_packet_core::{ + NetlinkDeserializable, NetlinkHeader, NetlinkMessage, NetlinkPayload, NetlinkSerializable, + NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, NLM_F_EXCL, NLM_F_REQUEST, +}; +use netlink_packet_route::{ + address::{AddressAttribute, AddressMessage}, + route::{ + RouteAddress, RouteAttribute, RouteHeader, RouteMessage, RouteProtocol, RouteScope, + RouteType, + }, + AddressFamily, RouteNetlinkMessage, +}; +use netlink_sys::{protocols::NETLINK_ROUTE, Socket, SocketAddr}; +use nix::{ + ifaddrs::getifaddrs, + libc::{self, ifreq, ioctl, Ioctl, SIOCGIFFLAGS, SIOCGIFMTU, SIOCSIFFLAGS, SIOCSIFMTU}, + net::if_::InterfaceFlags, + sys::socket::SockaddrLike as _, +}; +use pnet::ipnetwork::ip_mask_to_prefix; + +use super::{route::Route, Error, IfConfiguerTrait}; + +pub(crate) fn dummy_socket() -> Result { + Ok(std::net::UdpSocket::bind("0:0")?) +} + +fn build_ifreq(name: &str) -> ifreq { + let c_str = CString::new(name).unwrap(); + let mut ifr: ifreq = unsafe { std::mem::zeroed() }; + let name_bytes = c_str.as_bytes_with_nul(); + for (i, &b) in name_bytes.iter().enumerate() { + ifr.ifr_name[i] = b as libc::c_char; + } + ifr +} + +fn send_netlink_req( + req: T, + flags: u16, +) -> Result { + let mut socket = Socket::new(NETLINK_ROUTE)?; + socket.bind_auto()?; + socket.connect(&SocketAddr::new(0, 0))?; + + let mut req: NetlinkMessage = + NetlinkMessage::new(NetlinkHeader::default(), NetlinkPayload::InnerMessage(req)); + req.header.flags = flags; + + req.finalize(); + let mut buf = vec![0; req.header.length as _]; + req.serialize(&mut buf); + + tracing::debug!("net link request >>> {:?}", req); + socket.send(&buf, 0)?; + + Ok(socket) +} + +fn send_netlink_req_and_wait_one_resp( + req: T, + is_remove: bool, +) -> Result<(), Error> { + let socket = send_netlink_req( + req, + NLM_F_ACK | NLM_F_CREATE | NLM_F_REQUEST | if !is_remove { NLM_F_EXCL } else { 0 }, + )?; + let resp = socket.recv_from_full()?; + let ret = NetlinkMessage::::deserialize(&resp.0) + .with_context(|| "Failed to deserialize netlink message")?; + + tracing::debug!("net link response <<< {:?}", ret); + + match ret.payload { + NetlinkPayload::Error(e) => { + if e.code == NonZero::new(0) { + return Ok(()); + } else { + return Err(e.to_io().into()); + } + } + p => { + tracing::error!("Unexpected netlink response: {:?}", p); + return Err(anyhow::anyhow!("Unexpected netlink response").into()); + } + } +} + +fn addr_to_ip(addr: RouteAddress) -> Option { + match addr { + RouteAddress::Inet(addr) => Some(addr.into()), + RouteAddress::Inet6(addr) => Some(addr.into()), + _ => None, + } +} + +impl From for Route { + fn from(msg: RouteMessage) -> Self { + let mut gateway = None; + let mut source = None; + let mut source_hint = None; + let mut destination = None; + let mut ifindex = None; + let mut metric = None; + + for attr in msg.attributes { + match attr { + RouteAttribute::Source(addr) => { + source = addr_to_ip(addr); + } + RouteAttribute::PrefSource(addr) => { + source_hint = addr_to_ip(addr); + } + RouteAttribute::Destination(addr) => { + destination = addr_to_ip(addr); + } + RouteAttribute::Gateway(addr) => { + gateway = addr_to_ip(addr); + } + RouteAttribute::Oif(i) => { + ifindex = Some(i); + } + RouteAttribute::Priority(priority) => { + metric = Some(priority); + } + _ => {} + } + } + // rtnetlink gives None instead of 0.0.0.0 for the default route, but we'll convert to 0 here to make it match the other platforms + let destination = destination.unwrap_or_else(|| match msg.header.address_family { + AddressFamily::Inet => Ipv4Addr::UNSPECIFIED.into(), + AddressFamily::Inet6 => Ipv6Addr::UNSPECIFIED.into(), + _ => panic!("invalid destination family"), + }); + Self { + destination, + prefix: msg.header.destination_prefix_length, + source, + source_prefix: msg.header.source_prefix_length, + source_hint, + gateway, + ifindex, + table: msg.header.table, + metric, + } + } +} + +pub struct NetlinkIfConfiger {} + +impl NetlinkIfConfiger { + fn get_interface_index(name: &str) -> Result { + let name = CString::new(name).with_context(|| "failed to convert interface name")?; + match unsafe { libc::if_nametoindex(name.as_ptr()) } { + 0 => Err(std::io::Error::last_os_error().into()), + n => Ok(n), + } + } + + fn get_prefix_len(name: &str, ip: Ipv4Addr) -> Result { + let addrs = Self::list_addresses(name)?; + for addr in addrs { + if addr.address() == IpAddr::V4(ip) { + return Ok(addr.network_length()); + } + } + Err(Error::NotFound) + } + + fn remove_one_ip(name: &str, ip: Ipv4Addr, prefix_len: u8) -> Result<(), Error> { + let mut message = AddressMessage::default(); + message.header.prefix_len = prefix_len; + message.header.index = NetlinkIfConfiger::get_interface_index(name)?; + message.header.family = AddressFamily::Inet; + + message + .attributes + .push(AddressAttribute::Address(std::net::IpAddr::V4(ip))); + + send_netlink_req_and_wait_one_resp::( + RouteNetlinkMessage::DelAddress(message), + true, + ) + } + + pub(crate) fn mtu_op>( + name: &str, + op: T, + value: libc::c_int, + ) -> Result + where + >::Error: Debug, + { + let dummy_socket = dummy_socket()?; + + let mut ifr: ifreq = build_ifreq(name); + + unsafe { + ifr.ifr_ifru.ifru_mtu = value; + + // 使用ioctl获取MTU + if ioctl(dummy_socket.as_raw_fd(), op.try_into().unwrap(), &ifr) != 0 { + return Err(std::io::Error::last_os_error().into()); + } + } + + Ok(unsafe { ifr.ifr_ifru.ifru_mtu as u32 }) + } + + fn mtu(name: &str) -> Result { + Self::mtu_op(name, SIOCGIFMTU, 0) + } + + pub fn list_addresses(name: &str) -> Result, Error> { + let mut result = vec![]; + + for interface in getifaddrs() + .with_context(|| "failed to call getifaddrs")? + .filter(|x| x.interface_name == name) + { + let (Some(address), Some(netmask)) = (interface.address, interface.netmask) else { + continue; + }; + + use nix::sys::socket::AddressFamily::{Inet, Inet6}; + + let (address, netmask) = match (address.family(), netmask.family()) { + (Some(Inet), Some(Inet)) => ( + IpAddr::V4(address.as_sockaddr_in().unwrap().ip().into()), + IpAddr::V4(netmask.as_sockaddr_in().unwrap().ip().into()), + ), + (Some(Inet6), Some(Inet6)) => ( + IpAddr::V6(address.as_sockaddr_in6().unwrap().ip()), + IpAddr::V6(netmask.as_sockaddr_in6().unwrap().ip()), + ), + (_, _) => continue, + }; + + let prefix = ip_mask_to_prefix(netmask).unwrap(); + + result.push(IpInet::new(address, prefix).unwrap()); + } + Ok(result) + } + + pub(crate) fn set_flags_op>( + name: &str, + op: T, + flags: InterfaceFlags, + ) -> Result + where + >::Error: Debug, + { + let mut req = build_ifreq(name); + req.ifr_ifru.ifru_flags = flags.bits() as _; + + let socket = dummy_socket()?; + + unsafe { + if ioctl(socket.as_raw_fd(), op.try_into().unwrap(), &req) != 0 { + return Err(std::io::Error::last_os_error().into()); + } + Ok(InterfaceFlags::from_bits_truncate( + req.ifr_ifru.ifru_flags as _, + )) + } + } + + pub(crate) fn set_flags(name: &str, flags: InterfaceFlags) -> Result { + Self::set_flags_op(name, SIOCSIFFLAGS, flags) + } + + pub(crate) fn get_flags(name: &str) -> Result { + Self::set_flags_op(name, SIOCGIFFLAGS, InterfaceFlags::empty()) + } + + fn list_routes() -> Result, Error> { + let mut message = RouteMessage::default(); + + message.header.table = RouteHeader::RT_TABLE_UNSPEC; + message.header.protocol = RouteProtocol::Unspec; + + message.header.scope = RouteScope::Universe; + message.header.kind = RouteType::Unicast; + + message.header.address_family = AddressFamily::Inet; + message.header.destination_prefix_length = 0; + message.header.source_prefix_length = 0; + + let s = send_netlink_req( + RouteNetlinkMessage::GetRoute(message), + NLM_F_REQUEST | NLM_F_DUMP, + )?; + + let mut ret_vec = vec![]; + + let mut resp = Vec::::new(); + loop { + if resp.len() == 0 { + let (new_resp, _) = s.recv_from_full()?; + resp = new_resp; + } + let ret = NetlinkMessage::::deserialize(&resp) + .with_context(|| "Failed to deserialize netlink message")?; + resp = resp.split_off(ret.buffer_len()); + + tracing::debug!("net link response <<< {:?}", ret); + + match ret.payload { + NetlinkPayload::Error(e) => { + if e.code == NonZero::new(0) { + continue; + } else { + return Err(e.to_io().into()); + } + } + NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewRoute(m)) => { + tracing::debug!("net link response <<< {:?}", m); + ret_vec.push(m); + } + NetlinkPayload::Done(_) => { + break; + } + p => { + tracing::error!("Unexpected netlink response: {:?}", p); + return Err(anyhow::anyhow!("Unexpected netlink response").into()); + } + } + } + + Ok(ret_vec) + } +} + +#[async_trait] +impl IfConfiguerTrait for NetlinkIfConfiger { + async fn add_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + let mut message = RouteMessage::default(); + + message.header.table = RouteHeader::RT_TABLE_MAIN; + message.header.protocol = RouteProtocol::Static; + message.header.scope = RouteScope::Universe; + message.header.kind = RouteType::Unicast; + message.header.address_family = AddressFamily::Inet; + // metric + message.attributes.push(RouteAttribute::Priority(65535)); + // output interface + message + .attributes + .push(RouteAttribute::Oif(NetlinkIfConfiger::get_interface_index( + name, + )?)); + // source address + message.header.destination_prefix_length = cidr_prefix; + message + .attributes + .push(RouteAttribute::Destination(RouteAddress::Inet(address))); + + send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::NewRoute(message), false) + } + + async fn remove_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + let routes = Self::list_routes()?; + let ifidx = NetlinkIfConfiger::get_interface_index(name)?; + + for msg in routes { + let other_route: Route = msg.clone().into(); + if other_route.destination == std::net::IpAddr::V4(address) + && other_route.prefix == cidr_prefix + && other_route.ifindex == Some(ifidx) + { + send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::DelRoute(msg), true)?; + return Ok(()); + } + } + + Ok(()) + } + + async fn add_ipv4_ip( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + let mut message = AddressMessage::default(); + + message.header.prefix_len = cidr_prefix; + message.header.index = NetlinkIfConfiger::get_interface_index(name)?; + message.header.family = AddressFamily::Inet; + + message + .attributes + .push(AddressAttribute::Address(std::net::IpAddr::V4(address))); + + // for IPv4 the IFA_LOCAL address can be set to the same value as + // IFA_ADDRESS + message + .attributes + .push(AddressAttribute::Local(std::net::IpAddr::V4(address))); + + // set the IFA_BROADCAST address as well + if cidr_prefix == 32 { + message + .attributes + .push(AddressAttribute::Broadcast(address)); + } else { + let ip_addr = u32::from(address); + let brd = Ipv4Addr::from((0xffff_ffff_u32) >> u32::from(cidr_prefix) | ip_addr); + message.attributes.push(AddressAttribute::Broadcast(brd)); + }; + + send_netlink_req_and_wait_one_resp::( + RouteNetlinkMessage::NewAddress(message), + false, + ) + } + + async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> { + let mut flags = Self::get_flags(name)?; + flags.set(InterfaceFlags::IFF_UP, up); + Self::set_flags(name, flags)?; + Ok(()) + } + + async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> { + if ip.is_none() { + let addrs = Self::list_addresses(name)?; + for addr in addrs { + if let IpAddr::V4(ipv4) = addr.address() { + Self::remove_one_ip(name, ipv4, addr.network_length())?; + } + } + } else { + let ip = ip.unwrap(); + let prefix_len = Self::get_prefix_len(name, ip)?; + Self::remove_one_ip(name, ip, prefix_len)?; + } + + Ok(()) + } + + async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> { + Self::mtu_op(name, SIOCSIFMTU, mtu as libc::c_int)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const DUMMY_IFACE_NAME: &str = "dummy"; + + fn run_cmd(cmd: &str) -> String { + let output = std::process::Command::new("sh") + .arg("-c") + .arg(cmd) + .output() + .expect("failed to execute process"); + String::from_utf8(output.stdout).unwrap() + } + + struct PrepareEnv {} + impl PrepareEnv { + fn new() -> Self { + let _ = run_cmd(&format!("sudo ip link add {} type dummy", DUMMY_IFACE_NAME)); + PrepareEnv {} + } + } + + impl Drop for PrepareEnv { + fn drop(&mut self) { + let _ = run_cmd(&format!("sudo ip link del {}", DUMMY_IFACE_NAME)); + } + } + + #[serial_test::serial] + #[tokio::test] + async fn addr_test() { + let _prepare_env = PrepareEnv::new(); + let ifcfg = NetlinkIfConfiger {}; + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + ifcfg + .add_ipv4_ip(DUMMY_IFACE_NAME, "10.44.44.4".parse().unwrap(), 24) + .await + .unwrap(); + + let addrs = NetlinkIfConfiger::list_addresses(DUMMY_IFACE_NAME).unwrap(); + assert_eq!(addrs.len(), 1); + assert_eq!( + addrs[0].address(), + IpAddr::V4("10.44.44.4".parse().unwrap()) + ); + assert_eq!(addrs[0].network_length(), 24); + + NetlinkIfConfiger::remove_one_ip(DUMMY_IFACE_NAME, "10.44.44.4".parse().unwrap(), 24) + .unwrap(); + + let addrs = NetlinkIfConfiger::list_addresses(DUMMY_IFACE_NAME).unwrap(); + assert_eq!(addrs.len(), 0); + + let old_mtu = NetlinkIfConfiger::mtu(DUMMY_IFACE_NAME).unwrap(); + assert_ne!(old_mtu, 0); + + let new_mtu = old_mtu + 1; + ifcfg.set_mtu(DUMMY_IFACE_NAME, new_mtu).await.unwrap(); + + let mtu = NetlinkIfConfiger::mtu(DUMMY_IFACE_NAME).unwrap(); + assert_eq!(mtu, new_mtu); + + ifcfg + .set_link_status(DUMMY_IFACE_NAME, false) + .await + .unwrap(); + ifcfg.set_link_status(DUMMY_IFACE_NAME, true).await.unwrap(); + } + + #[serial_test::serial] + #[tokio::test] + async fn route_test() { + let _prepare_env = PrepareEnv::new(); + let ret = NetlinkIfConfiger::list_routes().unwrap(); + + let ifcfg = NetlinkIfConfiger {}; + println!("{:?}", ret); + + ifcfg.set_link_status(DUMMY_IFACE_NAME, true).await.unwrap(); + + ifcfg + .add_ipv4_route(DUMMY_IFACE_NAME, "10.5.5.0".parse().unwrap(), 24) + .await + .unwrap(); + + let routes = NetlinkIfConfiger::list_routes() + .unwrap() + .into_iter() + .map(Route::from) + .map(|x| x.destination) + .collect::>(); + assert!(routes.contains(&IpAddr::V4("10.5.5.0".parse().unwrap()))); + + ifcfg + .remove_ipv4_route(DUMMY_IFACE_NAME, "10.5.5.0".parse().unwrap(), 24) + .await + .unwrap(); + let routes = NetlinkIfConfiger::list_routes() + .unwrap() + .into_iter() + .map(Route::from) + .map(|x| x.destination) + .collect::>(); + assert!(!routes.contains(&IpAddr::V4("10.5.5.0".parse().unwrap()))); + } +} diff --git a/rust/easytier/src/common/ifcfg/route.rs b/rust/easytier/src/common/ifcfg/route.rs new file mode 100644 index 0000000..5e428b0 --- /dev/null +++ b/rust/easytier/src/common/ifcfg/route.rs @@ -0,0 +1,133 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Route { + /// Network address of the destination. `0.0.0.0` with a prefix of `0` is considered a default route. + pub destination: IpAddr, + + /// Length of network prefix in the destination address. + pub prefix: u8, + + /// The address of the next hop of this route. + /// + /// On macOS, this must be `Some` if ifindex is `None` + pub gateway: Option, + + /// The index of the local interface through which the next hop of this route may be reached. + /// + /// On macOS, this must be `Some` if gateway is `None` + pub ifindex: Option, + + #[cfg(target_os = "linux")] + /// The routing table this route belongs to. + pub table: u8, + + /// Network address of the source. + #[cfg(target_os = "linux")] + pub source: Option, + + /// Prefix length of the source address. + #[cfg(target_os = "linux")] + pub source_prefix: u8, + + /// Source address hint. Does not influence routing. + #[cfg(target_os = "linux")] + pub source_hint: Option, + + #[cfg(any(target_os = "windows", target_os = "linux"))] + /// The route metric offset value for this route. + pub metric: Option, + + #[cfg(target_os = "windows")] + /// Luid of the local interface through which the next hop of this route may be reached. + /// + /// If luid is specified, ifindex is optional. + pub luid: Option, +} + +impl Route { + /// Create a route that matches a given destination network. + /// + /// Either the gateway or interface should be set before attempting to add to a routing table. + pub fn new(destination: IpAddr, prefix: u8) -> Self { + Self { + destination, + prefix, + gateway: None, + ifindex: None, + #[cfg(target_os = "linux")] + // default to main table + table: 254, + #[cfg(target_os = "linux")] + source: None, + #[cfg(target_os = "linux")] + source_prefix: 0, + #[cfg(target_os = "linux")] + source_hint: None, + #[cfg(any(target_os = "windows", target_os = "linux"))] + metric: None, + #[cfg(target_os = "windows")] + luid: None, + } + } + + /// Set the next next hop gateway for this route. + pub fn with_gateway(mut self, gateway: IpAddr) -> Self { + self.gateway = Some(gateway); + self + } + + /// Set the index of the local interface through which the next hop of this route should be reached. + pub fn with_ifindex(mut self, ifindex: u32) -> Self { + self.ifindex = Some(ifindex); + self + } + + /// Set table the route will be installed in. + #[cfg(target_os = "linux")] + pub fn with_table(mut self, table: u8) -> Self { + self.table = table; + self + } + + /// Set source. + #[cfg(target_os = "linux")] + pub fn with_source(mut self, source: IpAddr, prefix: u8) -> Self { + self.source = Some(source); + self.source_prefix = prefix; + self + } + + /// Set source hint. + #[cfg(target_os = "linux")] + pub fn with_source_hint(mut self, hint: IpAddr) -> Self { + self.source_hint = Some(hint); + self + } + + /// Set route metric. + #[cfg(any(target_os = "windows", target_os = "linux"))] + pub fn with_metric(mut self, metric: u32) -> Self { + self.metric = Some(metric); + self + } + + /// Set luid of the local interface through which the next hop of this route should be reached. + #[cfg(target_os = "windows")] + pub fn with_luid(mut self, luid: u64) -> Self { + self.luid = Some(luid); + self + } + + /// Get the netmask covering the network portion of the destination address. + pub fn mask(&self) -> IpAddr { + match self.destination { + IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::from( + u32::MAX.checked_shl(32 - self.prefix as u32).unwrap_or(0), + )), + IpAddr::V6(_) => IpAddr::V6(Ipv6Addr::from( + u128::MAX.checked_shl(128 - self.prefix as u32).unwrap_or(0), + )), + } + } +} diff --git a/rust/easytier/src/common/ifcfg/windows.rs b/rust/easytier/src/common/ifcfg/windows.rs new file mode 100644 index 0000000..3abdc2c --- /dev/null +++ b/rust/easytier/src/common/ifcfg/windows.rs @@ -0,0 +1,166 @@ +use std::net::Ipv4Addr; + +use async_trait::async_trait; + +use super::{cidr_to_subnet_mask, run_shell_cmd, Error, IfConfiguerTrait}; + +pub struct WindowsIfConfiger {} + +impl WindowsIfConfiger { + pub fn get_interface_index(name: &str) -> Option { + crate::arch::windows::find_interface_index(name).ok() + } + + async fn list_ipv4(name: &str) -> Result, Error> { + use anyhow::Context; + use network_interface::NetworkInterfaceConfig; + use std::net::IpAddr; + let ret = network_interface::NetworkInterface::show().with_context(|| "show interface")?; + let addrs = ret + .iter() + .filter_map(|x| { + if x.name != name { + return None; + } + Some(x.addr.clone()) + }) + .flat_map(|x| x) + .map(|x| x.ip()) + .filter_map(|x| { + if let IpAddr::V4(ipv4) = x { + Some(ipv4) + } else { + None + } + }) + .collect::>(); + + Ok(addrs) + } + + async fn remove_one_ipv4(name: &str, ip: Ipv4Addr) -> Result<(), Error> { + run_shell_cmd( + format!( + "netsh interface ipv4 delete address {} address={}", + name, + ip.to_string() + ) + .as_str(), + ) + .await + } +} + +#[cfg(target_os = "windows")] +#[async_trait] +impl IfConfiguerTrait for WindowsIfConfiger { + async fn add_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + let Some(idx) = Self::get_interface_index(name) else { + return Err(Error::NotFound); + }; + run_shell_cmd( + format!( + "route ADD {} MASK {} 10.1.1.1 IF {} METRIC 9000", + address, + cidr_to_subnet_mask(cidr_prefix), + idx + ) + .as_str(), + ) + .await + } + + async fn remove_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + let Some(idx) = Self::get_interface_index(name) else { + return Err(Error::NotFound); + }; + run_shell_cmd( + format!( + "route DELETE {} MASK {} IF {}", + address, + cidr_to_subnet_mask(cidr_prefix), + idx + ) + .as_str(), + ) + .await + } + + async fn add_ipv4_ip( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "netsh interface ipv4 add address {} address={} mask={}", + name, + address, + cidr_to_subnet_mask(cidr_prefix) + ) + .as_str(), + ) + .await + } + + async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> { + run_shell_cmd( + format!( + "netsh interface set interface {} {}", + name, + if up { "enable" } else { "disable" } + ) + .as_str(), + ) + .await + } + + async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> { + if ip.is_none() { + for ip in Self::list_ipv4(name).await?.iter() { + Self::remove_one_ipv4(name, *ip).await?; + } + Ok(()) + } else { + Self::remove_one_ipv4(name, ip.unwrap()).await + } + } + + async fn wait_interface_show(&self, name: &str) -> Result<(), Error> { + Ok( + tokio::time::timeout(std::time::Duration::from_secs(10), async move { + loop { + if let Some(idx) = Self::get_interface_index(name) { + tracing::info!(?name, ?idx, "Interface found"); + break; + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + Ok::<(), Error>(()) + }) + .await??, + ) + } + + async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> { + let _ = run_shell_cmd( + format!("netsh interface ipv6 set subinterface {} mtu={}", name, mtu).as_str(), + ) + .await; + run_shell_cmd( + format!("netsh interface ipv4 set subinterface {} mtu={}", name, mtu).as_str(), + ) + .await + } +} diff --git a/rust/easytier/src/common/mod.rs b/rust/easytier/src/common/mod.rs new file mode 100644 index 0000000..a5bd69b --- /dev/null +++ b/rust/easytier/src/common/mod.rs @@ -0,0 +1,178 @@ +use std::{ + fmt::Debug, + future, + io::Write as _, + sync::{Arc, Mutex}, +}; +use tokio::{task::JoinSet, time::timeout}; +use tracing::Instrument; + +pub mod compressor; +pub mod config; +pub mod constants; +pub mod defer; +pub mod error; +pub mod global_ctx; +pub mod ifcfg; +pub mod netns; +pub mod network; +pub mod scoped_task; +pub mod stun; +pub mod stun_codec_ext; + +pub fn get_logger_timer( + format: F, +) -> tracing_subscriber::fmt::time::OffsetTime { + unsafe { + time::util::local_offset::set_soundness(time::util::local_offset::Soundness::Unsound) + }; + let local_offset = time::UtcOffset::current_local_offset() + .unwrap_or(time::UtcOffset::from_whole_seconds(0).unwrap()); + tracing_subscriber::fmt::time::OffsetTime::new(local_offset, format) +} + +pub fn get_logger_timer_rfc3339( +) -> tracing_subscriber::fmt::time::OffsetTime { + get_logger_timer(time::format_description::well_known::Rfc3339) +} + +pub type PeerId = u32; + +pub fn new_peer_id() -> PeerId { + rand::random() +} + +pub fn join_joinset_background( + js: Arc>>, + origin: String, +) { + let js = Arc::downgrade(&js); + let o = origin.clone(); + tokio::spawn( + async move { + while js.strong_count() > 0 { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let fut = future::poll_fn(|cx| { + let Some(js) = js.upgrade() else { + return std::task::Poll::Ready(()); + }; + + let mut js = js.lock().unwrap(); + while !js.is_empty() { + let ret = js.poll_join_next(cx); + match ret { + std::task::Poll::Ready(Some(_)) => { + continue; + } + std::task::Poll::Ready(None) => { + break; + } + std::task::Poll::Pending => { + return std::task::Poll::Pending; + } + } + } + std::task::Poll::Ready(()) + }); + + let _ = timeout(std::time::Duration::from_secs(5), fut).await; + } + tracing::debug!(?o, "joinset task exit"); + } + .instrument(tracing::info_span!( + "join_joinset_background", + origin = origin + )), + ); +} + +pub fn get_machine_id() -> uuid::Uuid { + // a path same as the binary + let machine_id_file = std::env::current_exe() + .map(|x| x.with_file_name("et_machine_id")) + .unwrap_or_else(|_| std::path::PathBuf::from("et_machine_id")); + + // try load from local file + if let Ok(mid) = std::fs::read_to_string(&machine_id_file) { + if let Ok(mid) = uuid::Uuid::parse_str(mid.trim()) { + return mid; + } + } + + #[cfg(any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "freebsd" + ))] + let gen_mid = machine_uid::get() + .map(|x| { + let mut b = [0u8; 16]; + crate::tunnel::generate_digest_from_str("", x.as_str(), &mut b); + uuid::Uuid::from_bytes(b) + }) + .ok(); + + #[cfg(not(any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "freebsd" + )))] + let gen_mid = None; + + if gen_mid.is_some() { + return gen_mid.unwrap(); + } + + let gen_mid = uuid::Uuid::new_v4(); + + // try save to local file + if let Ok(mut file) = std::fs::File::create(machine_id_file) { + let _ = file.write_all(gen_mid.to_string().as_bytes()); + } + + gen_mid +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_join_joinset_backgroud() { + let js = Arc::new(Mutex::new(JoinSet::<()>::new())); + join_joinset_background(js.clone(), "TEST".to_owned()); + js.try_lock().unwrap().spawn(async { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + }); + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + assert!(js.try_lock().unwrap().is_empty()); + + for _ in 0..5 { + js.try_lock().unwrap().spawn(async { + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + }); + tokio::task::yield_now().await; + } + + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + + for _ in 0..5 { + js.try_lock().unwrap().spawn(async { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + }); + tokio::task::yield_now().await; + } + + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + assert!(js.try_lock().unwrap().is_empty()); + + let weak_js = Arc::downgrade(&js); + drop(js); + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + assert_eq!(weak_js.weak_count(), 0); + assert_eq!(weak_js.strong_count(), 0); + } +} diff --git a/rust/easytier/src/common/netns.rs b/rust/easytier/src/common/netns.rs new file mode 100644 index 0000000..4f6ba3e --- /dev/null +++ b/rust/easytier/src/common/netns.rs @@ -0,0 +1,114 @@ +use futures::Future; + +#[cfg(target_os = "linux")] +use nix::sched::{setns, CloneFlags}; +#[cfg(target_os = "linux")] +use std::os::fd::AsFd; + +pub struct NetNSGuard { + #[cfg(target_os = "linux")] + old_ns: Option, +} + +pub static ROOT_NETNS_NAME: &str = "_root_ns"; + +#[cfg(target_os = "linux")] +impl NetNSGuard { + pub fn new(ns: Option) -> Box { + let old_ns = if ns.is_some() { + let old_ns = if cfg!(target_os = "linux") { + Some(std::fs::File::open("/proc/self/ns/net").unwrap()) + } else { + None + }; + Self::switch_ns(ns); + old_ns + } else { + None + }; + Box::new(NetNSGuard { old_ns }) + } + + fn switch_ns(name: Option) { + if name.is_none() { + return; + } + + let ns_path: String; + let name = name.unwrap(); + if name == ROOT_NETNS_NAME { + ns_path = "/proc/1/ns/net".to_string(); + } else { + ns_path = format!("/var/run/netns/{}", name); + } + + let ns = std::fs::File::open(ns_path).unwrap(); + tracing::info!( + "[INIT NS] switching to new ns_name: {:?}, ns_file: {:?}", + name, + ns + ); + + setns(ns.as_fd(), CloneFlags::CLONE_NEWNET).unwrap(); + } +} + +#[cfg(target_os = "linux")] +impl Drop for NetNSGuard { + fn drop(&mut self) { + if self.old_ns.is_none() { + return; + } + tracing::info!("[INIT NS] switching back to old ns, ns: {:?}", self.old_ns); + setns( + self.old_ns.as_ref().unwrap().as_fd(), + CloneFlags::CLONE_NEWNET, + ) + .unwrap(); + } +} + +#[cfg(not(target_os = "linux"))] +impl NetNSGuard { + pub fn new(_ns: Option) -> Box { + Box::new(NetNSGuard {}) + } +} + +#[derive(Clone, Debug)] +pub struct NetNS { + name: Option, +} + +impl NetNS { + pub fn new(name: Option) -> Self { + NetNS { name } + } + + pub async fn run_async(&self, f: F) -> Ret + where + F: FnOnce() -> Fut, + Fut: Future, + { + // TODO: do we really need this lock + // let _lock = LOCK.lock().await; + let _guard = NetNSGuard::new(self.name.clone()); + f().await + } + + pub fn run(&self, f: F) -> Ret + where + F: FnOnce() -> Ret, + { + let _guard = NetNSGuard::new(self.name.clone()); + f() + } + + pub fn guard(&self) -> Box { + NetNSGuard::new(self.name.clone()) + } + + pub fn name(&self) -> Option { + self.name.clone() + } +} diff --git a/rust/easytier/src/common/network.rs b/rust/easytier/src/common/network.rs new file mode 100644 index 0000000..5a0dbd8 --- /dev/null +++ b/rust/easytier/src/common/network.rs @@ -0,0 +1,281 @@ +use std::{net::IpAddr, ops::Deref, sync::Arc}; + +use pnet::datalink::NetworkInterface; +use tokio::{ + sync::{Mutex, RwLock}, + task::JoinSet, +}; + +use crate::proto::peer_rpc::GetIpListResponse; + +use super::{netns::NetNS, stun::StunInfoCollectorTrait}; + +pub const CACHED_IP_LIST_TIMEOUT_SEC: u64 = 60; + +struct InterfaceFilter { + iface: NetworkInterface, +} + +#[cfg(target_os = "android")] +impl InterfaceFilter { + async fn filter_iface(&self) -> bool { + true + } +} + +#[cfg(target_os = "linux")] +impl InterfaceFilter { + async fn is_tun_tap_device(&self) -> bool { + let path = format!("/sys/class/net/{}/tun_flags", self.iface.name); + tokio::fs::metadata(&path).await.is_ok() + } + + async fn has_valid_ip(&self) -> bool { + self.iface + .ips + .iter() + .map(|ip| ip.ip()) + .any(|ip| !ip.is_loopback() && !ip.is_unspecified() && !ip.is_multicast()) + } + + async fn filter_iface(&self) -> bool { + tracing::trace!( + "filter linux iface: {:?}, is_point_to_point: {}, is_loopback: {}, is_up: {}, is_lower_up: {}, is_tun: {}, has_valid_ip: {}", + self.iface, + self.iface.is_point_to_point(), + self.iface.is_loopback(), + self.iface.is_up(), + self.iface.is_lower_up(), + self.is_tun_tap_device().await, + self.has_valid_ip().await + ); + + !self.iface.is_point_to_point() + && !self.iface.is_loopback() + && self.iface.is_up() + && self.iface.is_lower_up() + && !self.is_tun_tap_device().await + && self.has_valid_ip().await + } +} + +#[cfg(any(target_os = "macos", target_os = "freebsd"))] +impl InterfaceFilter { + #[cfg(target_os = "macos")] + async fn is_interface_physical(&self) -> bool { + let interface_name = &self.iface.name; + let output = tokio::process::Command::new("networksetup") + .args(&["-listallhardwareports"]) + .output() + .await + .expect("Failed to execute command"); + + let stdout = std::str::from_utf8(&output.stdout).expect("Invalid UTF-8"); + + let lines: Vec<&str> = stdout.lines().collect(); + + for i in 0..lines.len() { + let line = lines[i]; + + if line.contains("Device:") && line.contains(interface_name) { + let next_line = lines[i + 1]; + if next_line.contains("Virtual Interface") { + return false; + } else { + return true; + } + } + } + + false + } + + #[cfg(target_os = "freebsd")] + async fn is_interface_physical(&self) -> bool { + // if mac addr is not zero, then it's physical interface + self.iface.mac.map(|mac| !mac.is_zero()).unwrap_or(false) + } + + async fn filter_iface(&self) -> bool { + !self.iface.is_point_to_point() + && !self.iface.is_loopback() + && self.iface.is_up() + && self.is_interface_physical().await + } +} + +#[cfg(target_os = "windows")] +impl InterfaceFilter { + async fn filter_iface(&self) -> bool { + tracing::debug!( + "iface_name: {:?}, p2p: {:?}, is_up: {:?}, iface: {:?}", + self.iface.name, + self.iface.is_point_to_point(), + self.iface.is_up(), + self.iface + ); + !self.iface.is_point_to_point() + && !self.iface.is_loopback() + && self + .iface + .ips + .iter() + .map(|ip| ip.ip()) + .any(|ip| !ip.is_loopback() && !ip.is_unspecified() && !ip.is_multicast()) + && self.iface.mac.map(|mac| !mac.is_zero()).unwrap_or(false) + } +} + +pub async fn local_ipv4() -> std::io::Result { + let socket = tokio::net::UdpSocket::bind("0.0.0.0:0").await?; + socket.connect("8.8.8.8:80").await?; + let addr = socket.local_addr()?; + match addr.ip() { + std::net::IpAddr::V4(ip) => Ok(ip), + std::net::IpAddr::V6(_) => Err(std::io::Error::new( + std::io::ErrorKind::AddrNotAvailable, + "no ipv4 address", + )), + } +} + +pub async fn local_ipv6() -> std::io::Result { + let socket = tokio::net::UdpSocket::bind("[::]:0").await?; + socket + .connect("[2001:4860:4860:0000:0000:0000:0000:8888]:80") + .await?; + let addr = socket.local_addr()?; + match addr.ip() { + std::net::IpAddr::V6(ip) => Ok(ip), + std::net::IpAddr::V4(_) => Err(std::io::Error::new( + std::io::ErrorKind::AddrNotAvailable, + "no ipv4 address", + )), + } +} + +pub struct IPCollector { + cached_ip_list: Arc>, + collect_ip_task: Mutex>, + net_ns: NetNS, + stun_info_collector: Arc>, +} + +impl IPCollector { + pub fn new(net_ns: NetNS, stun_info_collector: T) -> Self { + Self { + cached_ip_list: Arc::new(RwLock::new(GetIpListResponse::default())), + collect_ip_task: Mutex::new(JoinSet::new()), + net_ns, + stun_info_collector: Arc::new(Box::new(stun_info_collector)), + } + } + + pub async fn collect_ip_addrs(&self) -> GetIpListResponse { + let mut task = self.collect_ip_task.lock().await; + if task.is_empty() { + let cached_ip_list = self.cached_ip_list.clone(); + *cached_ip_list.write().await = + Self::do_collect_local_ip_addrs(self.net_ns.clone()).await; + let net_ns = self.net_ns.clone(); + let stun_info_collector = self.stun_info_collector.clone(); + task.spawn(async move { + loop { + let ip_addrs = Self::do_collect_local_ip_addrs(net_ns.clone()).await; + *cached_ip_list.write().await = ip_addrs; + tokio::time::sleep(std::time::Duration::from_secs(CACHED_IP_LIST_TIMEOUT_SEC)) + .await; + } + }); + + let cached_ip_list = self.cached_ip_list.clone(); + task.spawn(async move { + loop { + let stun_info = stun_info_collector.get_stun_info(); + for ip in stun_info.public_ip.iter() { + let Ok(ip_addr) = ip.parse::() else { + continue; + }; + + match ip_addr { + IpAddr::V4(v) => { + cached_ip_list.write().await.public_ipv4 = Some(v.into()) + } + IpAddr::V6(v) => { + cached_ip_list.write().await.public_ipv6 = Some(v.into()) + } + } + } + + let sleep_sec = if !cached_ip_list.read().await.public_ipv4.is_none() { + CACHED_IP_LIST_TIMEOUT_SEC + } else { + 3 + }; + tokio::time::sleep(std::time::Duration::from_secs(sleep_sec)).await; + } + }); + } + + return self.cached_ip_list.read().await.deref().clone(); + } + + pub async fn collect_interfaces(net_ns: NetNS) -> Vec { + let _g = net_ns.guard(); + let ifaces = pnet::datalink::interfaces(); + let mut ret = vec![]; + for iface in ifaces { + let f = InterfaceFilter { + iface: iface.clone(), + }; + + if !f.filter_iface().await { + continue; + } + + ret.push(iface); + } + + ret + } + + #[tracing::instrument(skip(net_ns))] + async fn do_collect_local_ip_addrs(net_ns: NetNS) -> GetIpListResponse { + let mut ret = GetIpListResponse::default(); + + let ifaces = Self::collect_interfaces(net_ns.clone()).await; + let _g = net_ns.guard(); + for iface in ifaces { + for ip in iface.ips { + let ip: std::net::IpAddr = ip.ip(); + if ip.is_loopback() || ip.is_multicast() { + continue; + } + match ip { + std::net::IpAddr::V4(v4) => { + ret.interface_ipv4s.push(v4.into()); + } + std::net::IpAddr::V6(v6) => { + ret.interface_ipv6s.push(v6.into()); + } + } + } + } + + if let Ok(v4_addr) = local_ipv4().await { + tracing::trace!("got local ipv4: {}", v4_addr); + if !ret.interface_ipv4s.contains(&v4_addr.into()) { + ret.interface_ipv4s.push(v4_addr.into()); + } + } + + if let Ok(v6_addr) = local_ipv6().await { + tracing::trace!("got local ipv6: {}", v6_addr); + if !ret.interface_ipv6s.contains(&v6_addr.into()) { + ret.interface_ipv6s.push(v6_addr.into()); + } + } + + ret + } +} diff --git a/rust/easytier/src/common/scoped_task.rs b/rust/easytier/src/common/scoped_task.rs new file mode 100644 index 0000000..5669008 --- /dev/null +++ b/rust/easytier/src/common/scoped_task.rs @@ -0,0 +1,134 @@ +//! This crate provides a wrapper type of Tokio's JoinHandle: `ScopedTask`, which aborts the task when it's dropped. +//! `ScopedTask` can still be awaited to join the child-task, and abort-on-drop will still trigger while it is being awaited. +//! +//! For example, if task A spawned task B but is doing something else, and task B is waiting for task C to join, +//! aborting A will also abort both B and C. + +use std::future::Future; +use std::ops::Deref; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::task::JoinHandle; + +#[derive(Debug)] +pub struct ScopedTask { + inner: JoinHandle, +} + +impl Drop for ScopedTask { + fn drop(&mut self) { + self.inner.abort() + } +} + +impl Future for ScopedTask { + type Output = as Future>::Output; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.inner).poll(cx) + } +} + +impl From> for ScopedTask { + fn from(inner: JoinHandle) -> Self { + Self { inner } + } +} + +impl Deref for ScopedTask { + type Target = JoinHandle; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[cfg(test)] +mod tests { + use super::ScopedTask; + use futures_util::future::pending; + use std::sync::{Arc, RwLock}; + use tokio::task::yield_now; + + struct Sentry(Arc>); + impl Drop for Sentry { + fn drop(&mut self) { + *self.0.write().unwrap() = true + } + } + + #[tokio::test] + async fn drop_while_not_waiting_for_join() { + let dropped = Arc::new(RwLock::new(false)); + let sentry = Sentry(dropped.clone()); + let task = ScopedTask::from(tokio::spawn(async move { + let _sentry = sentry; + pending::<()>().await + })); + yield_now().await; + assert!(!*dropped.read().unwrap()); + drop(task); + yield_now().await; + assert!(*dropped.read().unwrap()); + } + + #[tokio::test] + async fn drop_while_waiting_for_join() { + let dropped = Arc::new(RwLock::new(false)); + let sentry = Sentry(dropped.clone()); + let handle = tokio::spawn(async move { + ScopedTask::from(tokio::spawn(async move { + let _sentry = sentry; + pending::<()>().await + })) + .await + .unwrap() + }); + yield_now().await; + assert!(!*dropped.read().unwrap()); + handle.abort(); + yield_now().await; + assert!(*dropped.read().unwrap()); + } + + #[tokio::test] + async fn no_drop_only_join() { + assert_eq!( + ScopedTask::from(tokio::spawn(async { + yield_now().await; + 5 + })) + .await + .unwrap(), + 5 + ) + } + + #[tokio::test] + async fn manually_abort_before_drop() { + let dropped = Arc::new(RwLock::new(false)); + let sentry = Sentry(dropped.clone()); + let task = ScopedTask::from(tokio::spawn(async move { + let _sentry = sentry; + pending::<()>().await + })); + yield_now().await; + assert!(!*dropped.read().unwrap()); + task.abort(); + yield_now().await; + assert!(*dropped.read().unwrap()); + } + + #[tokio::test] + async fn manually_abort_then_join() { + let dropped = Arc::new(RwLock::new(false)); + let sentry = Sentry(dropped.clone()); + let task = ScopedTask::from(tokio::spawn(async move { + let _sentry = sentry; + pending::<()>().await + })); + yield_now().await; + assert!(!*dropped.read().unwrap()); + task.abort(); + yield_now().await; + assert!(task.await.is_err()); + } +} diff --git a/rust/easytier/src/common/stun.rs b/rust/easytier/src/common/stun.rs new file mode 100644 index 0000000..301efcd --- /dev/null +++ b/rust/easytier/src/common/stun.rs @@ -0,0 +1,867 @@ +use std::collections::BTreeSet; +use std::net::{IpAddr, SocketAddr}; +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, Instant}; + +use crate::proto::common::{NatType, StunInfo}; +use anyhow::Context; +use chrono::Local; +use crossbeam::atomic::AtomicCell; +use rand::seq::IteratorRandom; +use tokio::net::{lookup_host, UdpSocket}; +use tokio::sync::{broadcast, Mutex}; +use tokio::task::JoinSet; +use tracing::{Instrument, Level}; + +use bytecodec::{DecodeExt, EncodeExt}; +use stun_codec::rfc5389::methods::BINDING; +use stun_codec::{Message, MessageClass, MessageDecoder, MessageEncoder}; + +use crate::common::error::Error; + +use super::stun_codec_ext::*; + +struct HostResolverIter { + hostnames: Vec, + ips: Vec, + max_ip_per_domain: u32, +} + +impl HostResolverIter { + fn new(hostnames: Vec, max_ip_per_domain: u32) -> Self { + Self { + hostnames, + ips: vec![], + max_ip_per_domain, + } + } + + #[async_recursion::async_recursion] + async fn next(&mut self) -> Option { + if self.ips.is_empty() { + if self.hostnames.is_empty() { + return None; + } + + let host = self.hostnames.remove(0); + let host = if host.contains(':') { + host + } else { + format!("{}:3478", host) + }; + + match lookup_host(&host).await { + Ok(ips) => { + self.ips = ips + .filter(|x| x.is_ipv4()) + .choose_multiple(&mut rand::thread_rng(), self.max_ip_per_domain as usize); + + if self.ips.is_empty() { + return self.next().await; + } + } + Err(e) => { + tracing::warn!(?host, ?e, "lookup host for stun failed"); + return self.next().await; + } + }; + } + + Some(self.ips.remove(0)) + } +} + +#[derive(Debug, Clone)] +struct StunPacket { + data: Vec, + addr: SocketAddr, +} + +type StunPacketReceiver = tokio::sync::broadcast::Receiver; + +#[derive(Debug, Clone, Copy)] +struct BindRequestResponse { + local_addr: SocketAddr, + stun_server_addr: SocketAddr, + + recv_from_addr: SocketAddr, + mapped_socket_addr: Option, + changed_socket_addr: Option, + + change_ip: bool, + change_port: bool, + + real_ip_changed: bool, + real_port_changed: bool, + + latency_us: u32, +} + +impl BindRequestResponse { + pub fn get_mapped_addr_no_check(&self) -> &SocketAddr { + self.mapped_socket_addr.as_ref().unwrap() + } +} + +#[derive(Debug, Clone)] +struct StunClient { + stun_server: SocketAddr, + resp_timeout: Duration, + req_repeat: u32, + socket: Arc, + stun_packet_receiver: Arc>, +} + +impl StunClient { + pub fn new( + stun_server: SocketAddr, + socket: Arc, + stun_packet_receiver: StunPacketReceiver, + ) -> Self { + Self { + stun_server, + resp_timeout: Duration::from_millis(3000), + req_repeat: 2, + socket, + stun_packet_receiver: Arc::new(Mutex::new(stun_packet_receiver)), + } + } + + #[tracing::instrument(skip(self, buf))] + async fn wait_stun_response<'a, const N: usize>( + &self, + buf: &'a mut [u8; N], + tids: &Vec, + expected_ip_changed: bool, + expected_port_changed: bool, + stun_host: &SocketAddr, + ) -> Result<(Message, SocketAddr), Error> { + let mut now = tokio::time::Instant::now(); + let deadline = now + self.resp_timeout; + + while now < deadline { + let mut locked_receiver = self.stun_packet_receiver.lock().await; + let stun_packet_raw = tokio::time::timeout(deadline - now, locked_receiver.recv()) + .await? + .with_context(|| "recv stun packet from broadcast channel error")?; + now = tokio::time::Instant::now(); + + let (len, remote_addr) = (stun_packet_raw.data.len(), stun_packet_raw.addr); + + if len < 20 { + continue; + } + + let udp_buf = stun_packet_raw.data; + + // TODO:: we cannot borrow `buf` directly in udp recv_from, so we copy it here + unsafe { std::ptr::copy(udp_buf.as_ptr(), buf.as_ptr() as *mut u8, len) }; + + let mut decoder = MessageDecoder::::new(); + let Ok(msg) = decoder + .decode_from_bytes(&buf[..len]) + .with_context(|| format!("decode stun msg {:?}", buf))? + else { + continue; + }; + + tracing::trace!(b = ?&udp_buf[..len], ?tids, ?remote_addr, ?stun_host, "recv stun response, msg: {:#?}", msg); + + if msg.class() != MessageClass::SuccessResponse + || msg.method() != BINDING + || !tids.contains(&tid_to_u32(&msg.transaction_id())) + { + continue; + } + + return Ok((msg, remote_addr)); + } + + Err(Error::Unknown) + } + + fn extrace_mapped_addr(msg: &Message) -> Option { + let mut mapped_addr = None; + for x in msg.attributes() { + match x { + Attribute::MappedAddress(addr) => { + if mapped_addr.is_none() { + let _ = mapped_addr.insert(addr.address()); + } + } + Attribute::XorMappedAddress(addr) => { + if mapped_addr.is_none() { + let _ = mapped_addr.insert(addr.address()); + } + } + _ => {} + } + } + mapped_addr + } + + fn extract_changed_addr(msg: &Message) -> Option { + let mut changed_addr = None; + for x in msg.attributes() { + match x { + Attribute::OtherAddress(m) => { + if changed_addr.is_none() { + let _ = changed_addr.insert(m.address()); + } + } + Attribute::ChangedAddress(m) => { + if changed_addr.is_none() { + let _ = changed_addr.insert(m.address()); + } + } + _ => {} + } + } + changed_addr + } + + #[tracing::instrument(ret, level = Level::TRACE)] + pub async fn bind_request( + self, + change_ip: bool, + change_port: bool, + ) -> Result { + let stun_host = self.stun_server; + // repeat req in case of packet loss + let mut tids = vec![]; + + for _ in 0..self.req_repeat { + let tid = rand::random::(); + // let tid = 1; + let mut buf = [0u8; 28]; + // memset buf + unsafe { std::ptr::write_bytes(buf.as_mut_ptr(), 0, buf.len()) }; + + let mut message = + Message::::new(MessageClass::Request, BINDING, u32_to_tid(tid)); + message.add_attribute(ChangeRequest::new(change_ip, change_port)); + + // Encodes the message + let mut encoder = MessageEncoder::new(); + let msg = encoder + .encode_into_bytes(message.clone()) + .with_context(|| "encode stun message")?; + tids.push(tid); + tracing::trace!(?message, ?msg, tid, "send stun request"); + self.socket + .send_to(msg.as_slice().into(), &stun_host) + .await?; + } + + let now = Instant::now(); + + tracing::trace!("waiting stun response"); + let mut buf = [0; 1620]; + let (msg, recv_addr) = self + .wait_stun_response(&mut buf, &tids, change_ip, change_port, &stun_host) + .await?; + + let changed_socket_addr = Self::extract_changed_addr(&msg); + let real_ip_changed = stun_host.ip() != recv_addr.ip(); + let real_port_changed = stun_host.port() != recv_addr.port(); + + let resp = BindRequestResponse { + local_addr: self.socket.local_addr()?, + stun_server_addr: stun_host, + recv_from_addr: recv_addr, + mapped_socket_addr: Self::extrace_mapped_addr(&msg), + changed_socket_addr, + change_ip, + change_port, + + real_ip_changed, + real_port_changed, + + latency_us: now.elapsed().as_micros() as u32, + }; + + tracing::trace!( + ?stun_host, + ?recv_addr, + ?changed_socket_addr, + "finish stun bind request" + ); + + Ok(resp) + } +} + +struct StunClientBuilder { + udp: Arc, + task_set: JoinSet<()>, + stun_packet_sender: broadcast::Sender, +} + +impl StunClientBuilder { + pub fn new(udp: Arc) -> Self { + let (stun_packet_sender, _) = broadcast::channel(1024); + let mut task_set = JoinSet::new(); + + let udp_clone = udp.clone(); + let stun_packet_sender_clone = stun_packet_sender.clone(); + task_set.spawn( + async move { + let mut buf = [0; 1620]; + tracing::trace!("start stun packet listener"); + loop { + let Ok((len, addr)) = udp_clone.recv_from(&mut buf).await else { + tracing::error!("udp recv_from error"); + break; + }; + let data = buf[..len].to_vec(); + tracing::trace!(?addr, ?data, "recv udp stun packet"); + let _ = stun_packet_sender_clone.send(StunPacket { data, addr }); + } + } + .instrument(tracing::info_span!("stun_packet_listener")), + ); + + Self { + udp, + task_set, + stun_packet_sender, + } + } + + pub fn new_stun_client(&self, stun_server: SocketAddr) -> StunClient { + StunClient::new( + stun_server, + self.udp.clone(), + self.stun_packet_sender.subscribe(), + ) + } + + pub async fn stop(&mut self) { + self.task_set.abort_all(); + while let Some(_) = self.task_set.join_next().await {} + } +} + +#[derive(Debug, Clone)] +pub struct UdpNatTypeDetectResult { + source_addr: SocketAddr, + stun_resps: Vec, + // if we are easy symmetric nat, we need to test with another port to check inc or dec + extra_bind_test: Option, +} + +impl UdpNatTypeDetectResult { + fn new(source_addr: SocketAddr, stun_resps: Vec) -> Self { + Self { + source_addr, + stun_resps, + extra_bind_test: None, + } + } + + fn has_ip_changed_resp(&self) -> bool { + for resp in self.stun_resps.iter() { + if resp.real_ip_changed { + return true; + } + } + false + } + + fn has_port_changed_resp(&self) -> bool { + for resp in self.stun_resps.iter() { + if resp.real_port_changed { + return true; + } + } + false + } + + fn is_open_internet(&self) -> bool { + for resp in self.stun_resps.iter() { + if resp.mapped_socket_addr == Some(self.source_addr) { + return true; + } + } + return false; + } + + fn is_pat(&self) -> bool { + for resp in self.stun_resps.iter() { + if resp.mapped_socket_addr.map(|x| x.port()) == Some(self.source_addr.port()) { + return true; + } + } + false + } + + fn stun_server_count(&self) -> usize { + // find resp with distinct stun server + self.stun_resps + .iter() + .map(|x| x.stun_server_addr) + .collect::>() + .len() + } + + fn is_cone(&self) -> bool { + // if unique mapped addr count is less than stun server count, it is cone + let mapped_addr_count = self + .stun_resps + .iter() + .filter_map(|x| x.mapped_socket_addr) + .collect::>() + .len(); + mapped_addr_count == 1 + } + + pub fn nat_type(&self) -> NatType { + if self.stun_server_count() < 2 { + return NatType::Unknown; + } + + if self.is_cone() { + if self.has_ip_changed_resp() { + if self.is_open_internet() { + return NatType::OpenInternet; + } else if self.is_pat() { + return NatType::NoPat; + } else { + return NatType::FullCone; + } + } else if self.has_port_changed_resp() { + return NatType::Restricted; + } else { + return NatType::PortRestricted; + } + } else if !self.stun_resps.is_empty() { + if self.public_ips().len() != 1 + || self.usable_stun_resp_count() <= 1 + || self.max_port() - self.min_port() > 15 + || self.extra_bind_test.is_none() + || self + .extra_bind_test + .as_ref() + .unwrap() + .mapped_socket_addr + .is_none() + { + return NatType::Symmetric; + } else { + let extra_bind_test = self.extra_bind_test.as_ref().unwrap(); + let extra_port = extra_bind_test.mapped_socket_addr.unwrap().port(); + + let max_port_diff = extra_port.saturating_sub(self.max_port()); + let min_port_diff = self.min_port().saturating_sub(extra_port); + if max_port_diff != 0 && max_port_diff < 100 { + return NatType::SymmetricEasyInc; + } else if min_port_diff != 0 && min_port_diff < 100 { + return NatType::SymmetricEasyDec; + } else { + return NatType::Symmetric; + } + } + } else { + return NatType::Unknown; + } + } + + pub fn public_ips(&self) -> Vec { + self.stun_resps + .iter() + .filter_map(|x| x.mapped_socket_addr.map(|x| x.ip())) + .collect::>() + .into_iter() + .collect() + } + + pub fn collect_available_stun_server(&self) -> Vec { + let mut ret = vec![]; + for resp in self.stun_resps.iter() { + if !ret.contains(&resp.stun_server_addr) { + ret.push(resp.stun_server_addr); + } + } + ret + } + + pub fn local_addr(&self) -> SocketAddr { + self.source_addr + } + + pub fn extend_result(&mut self, other: UdpNatTypeDetectResult) { + self.stun_resps.extend(other.stun_resps); + } + + pub fn min_port(&self) -> u16 { + self.stun_resps + .iter() + .filter_map(|x| x.mapped_socket_addr.map(|x| x.port())) + .min() + .unwrap_or(0) + } + + pub fn max_port(&self) -> u16 { + self.stun_resps + .iter() + .filter_map(|x| x.mapped_socket_addr.map(|x| x.port())) + .max() + .unwrap_or(u16::MAX) + } + + pub fn usable_stun_resp_count(&self) -> usize { + self.stun_resps + .iter() + .filter(|x| x.mapped_socket_addr.is_some()) + .count() + } +} + +pub struct UdpNatTypeDetector { + stun_server_hosts: Vec, + max_ip_per_domain: u32, +} + +impl UdpNatTypeDetector { + pub fn new(stun_server_hosts: Vec, max_ip_per_domain: u32) -> Self { + Self { + stun_server_hosts, + max_ip_per_domain, + } + } + + async fn get_extra_bind_result( + &self, + source_port: u16, + stun_server: SocketAddr, + ) -> Result { + let udp = Arc::new(UdpSocket::bind(format!("0.0.0.0:{}", source_port)).await?); + let client_builder = StunClientBuilder::new(udp.clone()); + client_builder + .new_stun_client(stun_server) + .bind_request(false, false) + .await + } + + pub async fn detect_nat_type(&self, source_port: u16) -> Result { + let udp = Arc::new(UdpSocket::bind(format!("0.0.0.0:{}", source_port)).await?); + self.detect_nat_type_with_socket(udp).await + } + + #[tracing::instrument(skip(self))] + pub async fn detect_nat_type_with_socket( + &self, + udp: Arc, + ) -> Result { + let mut stun_servers = vec![]; + let mut host_resolver = + HostResolverIter::new(self.stun_server_hosts.clone(), self.max_ip_per_domain); + while let Some(addr) = host_resolver.next().await { + stun_servers.push(addr); + } + + let client_builder = StunClientBuilder::new(udp.clone()); + let mut stun_task_set = JoinSet::new(); + + for stun_server in stun_servers.iter() { + stun_task_set.spawn( + client_builder + .new_stun_client(*stun_server) + .bind_request(false, false), + ); + stun_task_set.spawn( + client_builder + .new_stun_client(*stun_server) + .bind_request(false, true), + ); + stun_task_set.spawn( + client_builder + .new_stun_client(*stun_server) + .bind_request(true, true), + ); + } + + let mut bind_resps = vec![]; + while let Some(resp) = stun_task_set.join_next().await { + if let Ok(Ok(resp)) = resp { + bind_resps.push(resp); + } + } + + Ok(UdpNatTypeDetectResult::new(udp.local_addr()?, bind_resps)) + } +} + +#[async_trait::async_trait] +#[auto_impl::auto_impl(&, Arc, Box)] +pub trait StunInfoCollectorTrait: Send + Sync { + fn get_stun_info(&self) -> StunInfo; + async fn get_udp_port_mapping(&self, local_port: u16) -> Result; +} + +pub struct StunInfoCollector { + stun_servers: Arc>>, + udp_nat_test_result: Arc>>, + nat_test_result_time: Arc>>, + redetect_notify: Arc, + tasks: std::sync::Mutex>, + started: AtomicBool, +} + +#[async_trait::async_trait] +impl StunInfoCollectorTrait for StunInfoCollector { + fn get_stun_info(&self) -> StunInfo { + self.start_stun_routine(); + + let Some(result) = self.udp_nat_test_result.read().unwrap().clone() else { + return Default::default(); + }; + StunInfo { + udp_nat_type: result.nat_type() as i32, + tcp_nat_type: 0, + last_update_time: self.nat_test_result_time.load().timestamp(), + public_ip: result.public_ips().iter().map(|x| x.to_string()).collect(), + min_port: result.min_port() as u32, + max_port: result.max_port() as u32, + } + } + + async fn get_udp_port_mapping(&self, local_port: u16) -> Result { + self.start_stun_routine(); + + let mut stun_servers = self + .udp_nat_test_result + .read() + .unwrap() + .clone() + .map(|x| x.collect_available_stun_server()) + .unwrap_or(vec![]); + + if stun_servers.is_empty() { + let mut host_resolver = + HostResolverIter::new(self.stun_servers.read().unwrap().clone(), 2); + while let Some(addr) = host_resolver.next().await { + stun_servers.push(addr); + if stun_servers.len() >= 2 { + break; + } + } + } + + if stun_servers.is_empty() { + return Err(Error::NotFound); + } + + let udp = Arc::new(UdpSocket::bind(format!("0.0.0.0:{}", local_port)).await?); + let mut client_builder = StunClientBuilder::new(udp.clone()); + + for server in stun_servers.iter() { + let Ok(ret) = client_builder + .new_stun_client(*server) + .bind_request(false, false) + .await + else { + tracing::warn!(?server, "stun bind request failed"); + continue; + }; + if let Some(mapped_addr) = ret.mapped_socket_addr { + // make sure udp socket is available after return ok. + client_builder.stop().await; + return Ok(mapped_addr); + } + } + + Err(Error::NotFound) + } +} + +impl StunInfoCollector { + pub fn new(stun_servers: Vec) -> Self { + Self { + stun_servers: Arc::new(RwLock::new(stun_servers)), + udp_nat_test_result: Arc::new(RwLock::new(None)), + nat_test_result_time: Arc::new(AtomicCell::new(Local::now())), + redetect_notify: Arc::new(tokio::sync::Notify::new()), + tasks: std::sync::Mutex::new(JoinSet::new()), + started: AtomicBool::new(false), + } + } + + pub fn new_with_default_servers() -> Self { + Self::new(Self::get_default_servers()) + } + + pub fn get_default_servers() -> Vec { + // NOTICE: we may need to choose stun stun server based on geo location + // stun server cross nation may return a external ip address with high latency and loss rate + vec![ + "stun.miwifi.com", + "stun.chat.bilibili.com", + "stun.hitv.com", + "stun.cdnbye.com", + "stun.douyucdn.cn:18000", + "fwa.lifesizecloud.com", + "global.turn.twilio.com", + "turn.cloudflare.com", + "stun.isp.net.au", + "stun.nextcloud.com", + "stun.freeswitch.org", + "stun.voip.blackberry.com", + "stunserver.stunprotocol.org", + "stun.sipnet.com", + "stun.radiojar.com", + "stun.sonetel.com", + ] + .iter() + .map(|x| x.to_string()) + .collect() + } + + fn start_stun_routine(&self) { + if self.started.load(std::sync::atomic::Ordering::Relaxed) { + return; + } + self.started + .store(true, std::sync::atomic::Ordering::Relaxed); + + let stun_servers = self.stun_servers.clone(); + let udp_nat_test_result = self.udp_nat_test_result.clone(); + let udp_test_time = self.nat_test_result_time.clone(); + let redetect_notify = self.redetect_notify.clone(); + self.tasks.lock().unwrap().spawn(async move { + loop { + let servers = stun_servers.read().unwrap().clone(); + // use first three and random choose one from the rest + let servers = servers + .iter() + .take(2) + .chain(servers.iter().skip(2).choose(&mut rand::thread_rng())) + .map(|x| x.to_string()) + .collect(); + let detector = UdpNatTypeDetector::new(servers, 1); + let mut ret = detector.detect_nat_type(0).await; + tracing::debug!(?ret, "finish udp nat type detect"); + + let mut nat_type = NatType::Unknown; + if let Ok(resp) = &ret { + tracing::debug!(?resp, "got udp nat type detect result"); + nat_type = resp.nat_type(); + } + + // if nat type is symmtric, detect with another port to gather more info + if nat_type == NatType::Symmetric { + let old_resp = ret.as_mut().unwrap(); + tracing::debug!(?old_resp, "start get extra bind result"); + let available_stun_servers = old_resp.collect_available_stun_server(); + for server in available_stun_servers.iter() { + let ret = detector + .get_extra_bind_result(0, *server) + .await + .with_context(|| "get extra bind result failed"); + tracing::debug!(?ret, "finish udp nat type detect with another port"); + if let Ok(resp) = ret { + old_resp.extra_bind_test = Some(resp); + break; + } + } + } + + let mut sleep_sec = 10; + if let Ok(resp) = &ret { + udp_test_time.store(Local::now()); + *udp_nat_test_result.write().unwrap() = Some(resp.clone()); + if nat_type != NatType::Unknown + && (nat_type != NatType::Symmetric || resp.extra_bind_test.is_some()) + { + sleep_sec = 600 + } + } + + tokio::select! { + _ = redetect_notify.notified() => {} + _ = tokio::time::sleep(Duration::from_secs(sleep_sec)) => {} + } + } + }); + } + + pub fn update_stun_info(&self) { + self.redetect_notify.notify_one(); + } +} + +pub struct MockStunInfoCollector { + pub udp_nat_type: NatType, +} + +#[async_trait::async_trait] +impl StunInfoCollectorTrait for MockStunInfoCollector { + fn get_stun_info(&self) -> StunInfo { + StunInfo { + udp_nat_type: self.udp_nat_type as i32, + tcp_nat_type: NatType::Unknown as i32, + last_update_time: std::time::Instant::now().elapsed().as_secs() as i64, + min_port: 100, + max_port: 200, + public_ip: vec!["127.0.0.1".to_string()], + } + } + + async fn get_udp_port_mapping(&self, mut port: u16) -> Result { + if port == 0 { + port = 40144; + } + Ok(format!("127.0.0.1:{}", port).parse().unwrap()) + } +} + +#[cfg(test)] +mod tests { + use crate::tunnel::{udp::UdpTunnelListener, TunnelListener}; + + use super::*; + + #[tokio::test] + async fn test_udp_nat_type_detector() { + let collector = StunInfoCollector::new_with_default_servers(); + collector.update_stun_info(); + loop { + let ret = collector.get_stun_info(); + if ret.udp_nat_type != NatType::Unknown as i32 { + println!("{:#?}", ret); + break; + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + + let port_mapping = collector.get_udp_port_mapping(3000).await; + println!("{:#?}", port_mapping); + } + + #[tokio::test] + async fn test_internal_stun_server() { + let mut udp_server1 = UdpTunnelListener::new("udp://0.0.0.0:55555".parse().unwrap()); + let mut udp_server2 = UdpTunnelListener::new("udp://0.0.0.0:55535".parse().unwrap()); + + let mut tasks = JoinSet::new(); + tasks.spawn(async move { + udp_server1.listen().await.unwrap(); + loop { + udp_server1.accept().await.unwrap(); + } + }); + tasks.spawn(async move { + udp_server2.listen().await.unwrap(); + loop { + udp_server2.accept().await.unwrap(); + } + }); + + let stun_servers = vec!["127.0.0.1:55555".to_string(), "127.0.0.1:55535".to_string()]; + let detector = UdpNatTypeDetector::new(stun_servers, 1); + let ret = detector.detect_nat_type(0).await; + println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type()); + assert_eq!(ret.unwrap().nat_type(), NatType::PortRestricted); + } +} diff --git a/rust/easytier/src/common/stun_codec_ext.rs b/rust/easytier/src/common/stun_codec_ext.rs new file mode 100644 index 0000000..c013995 --- /dev/null +++ b/rust/easytier/src/common/stun_codec_ext.rs @@ -0,0 +1,300 @@ +use std::net::SocketAddr; + +use bytecodec::fixnum::{U32beDecoder, U32beEncoder}; +use stun_codec::net::{socket_addr_xor, SocketAddrDecoder, SocketAddrEncoder}; + +use stun_codec::rfc5389::attributes::{ + MappedAddress, Software, XorMappedAddress, XorMappedAddress2, +}; +use stun_codec::rfc5780::attributes::{OtherAddress, ResponseOrigin}; +use stun_codec::{define_attribute_enums, AttributeType, Message, TransactionId}; + +use bytecodec::{ByteCount, Decode, Encode, Eos, Result, SizedEncode, TryTaggedDecode}; + +use stun_codec::macros::track; + +macro_rules! impl_decode { + ($decoder:ty, $item:ident, $and_then:expr) => { + impl Decode for $decoder { + type Item = $item; + + fn decode(&mut self, buf: &[u8], eos: Eos) -> Result { + track!(self.0.decode(buf, eos)) + } + + fn finish_decoding(&mut self) -> Result { + track!(self.0.finish_decoding()).and_then($and_then) + } + + fn requiring_bytes(&self) -> ByteCount { + self.0.requiring_bytes() + } + + fn is_idle(&self) -> bool { + self.0.is_idle() + } + } + impl TryTaggedDecode for $decoder { + type Tag = AttributeType; + + fn try_start_decoding(&mut self, attr_type: Self::Tag) -> Result { + Ok(attr_type.as_u16() == $item::CODEPOINT) + } + } + }; +} + +macro_rules! impl_encode { + ($encoder:ty, $item:ty, $map_from:expr) => { + impl Encode for $encoder { + type Item = $item; + + fn encode(&mut self, buf: &mut [u8], eos: Eos) -> Result { + track!(self.0.encode(buf, eos)) + } + + #[allow(clippy::redundant_closure_call)] + fn start_encoding(&mut self, item: Self::Item) -> Result<()> { + track!(self.0.start_encoding($map_from(item))) + } + + fn requiring_bytes(&self) -> ByteCount { + self.0.requiring_bytes() + } + + fn is_idle(&self) -> bool { + self.0.is_idle() + } + } + impl SizedEncode for $encoder { + fn exact_requiring_bytes(&self) -> u64 { + self.0.exact_requiring_bytes() + } + } + }; +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ChangedAddress(SocketAddr); +impl ChangedAddress { + /// The codepoint of the type of the attribute. + pub const CODEPOINT: u16 = 0x0005; + + pub fn new(addr: SocketAddr) -> Self { + ChangedAddress(addr) + } + + /// Returns the address of this instance. + pub fn address(&self) -> SocketAddr { + self.0 + } +} +impl stun_codec::Attribute for ChangedAddress { + type Decoder = ChangedAddressDecoder; + type Encoder = ChangedAddressEncoder; + + fn get_type(&self) -> AttributeType { + AttributeType::new(Self::CODEPOINT) + } + + fn before_encode( + &mut self, + message: &Message, + ) -> bytecodec::Result<()> { + self.0 = socket_addr_xor(self.0, message.transaction_id()); + Ok(()) + } + + fn after_decode( + &mut self, + message: &Message, + ) -> bytecodec::Result<()> { + self.0 = socket_addr_xor(self.0, message.transaction_id()); + Ok(()) + } +} + +#[derive(Debug, Default)] +pub struct ChangedAddressDecoder(SocketAddrDecoder); +impl ChangedAddressDecoder { + pub fn new() -> Self { + Self::default() + } +} +impl_decode!(ChangedAddressDecoder, ChangedAddress, |item| Ok( + ChangedAddress(item) +)); + +#[derive(Debug, Default)] +pub struct ChangedAddressEncoder(SocketAddrEncoder); +impl ChangedAddressEncoder { + pub fn new() -> Self { + Self::default() + } +} +impl_encode!(ChangedAddressEncoder, ChangedAddress, |item: Self::Item| { + item.0 +}); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SourceAddress(SocketAddr); +impl SourceAddress { + /// The codepoint of the type of the attribute. + pub const CODEPOINT: u16 = 0x0004; + + pub fn new(addr: SocketAddr) -> Self { + SourceAddress(addr) + } + + /// Returns the address of this instance. + pub fn address(&self) -> SocketAddr { + self.0 + } +} +impl stun_codec::Attribute for SourceAddress { + type Decoder = SourceAddressDecoder; + type Encoder = SourceAddressEncoder; + + fn get_type(&self) -> AttributeType { + AttributeType::new(Self::CODEPOINT) + } + + fn before_encode( + &mut self, + message: &Message, + ) -> bytecodec::Result<()> { + self.0 = socket_addr_xor(self.0, message.transaction_id()); + Ok(()) + } + + fn after_decode( + &mut self, + message: &Message, + ) -> bytecodec::Result<()> { + self.0 = socket_addr_xor(self.0, message.transaction_id()); + Ok(()) + } +} + +#[derive(Debug, Default)] +pub struct SourceAddressDecoder(SocketAddrDecoder); +impl SourceAddressDecoder { + pub fn new() -> Self { + Self::default() + } +} +impl_decode!(SourceAddressDecoder, SourceAddress, |item| Ok( + SourceAddress(item) +)); + +#[derive(Debug, Default)] +pub struct SourceAddressEncoder(SocketAddrEncoder); +impl SourceAddressEncoder { + pub fn new() -> Self { + Self::default() + } +} +impl_encode!(SourceAddressEncoder, SourceAddress, |item: Self::Item| { + item.0 +}); + +/// `CHANGE-REQUEST` attribute. +/// +/// See [RFC 5780 -- 7.2. CHANGE-REQUEST] about this attribute. +/// +/// [RFC 5780 -- 7.2. CHANGE-REQUEST]: https://tools.ietf.org/html/rfc5780#section-7.2 +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ChangeRequest(bool, bool); + +impl ChangeRequest { + /// The codepoint of the type of the attribute. + pub const CODEPOINT: u16 = 0x0003; + + /// Makes a new `ChangeRequest` instance. + pub fn new(ip: bool, port: bool) -> Self { + ChangeRequest(ip, port) + } + + /// Returns whether the client requested the server to send the Binding Response with a + /// different IP address than the one the Binding Request was received on + pub fn ip(&self) -> bool { + self.0 + } + + /// Returns whether the client requested the server to send the Binding Response with a + /// different port than the one the Binding Request was received on + pub fn port(&self) -> bool { + self.1 + } +} + +impl stun_codec::Attribute for ChangeRequest { + type Decoder = ChangeRequestDecoder; + type Encoder = ChangeRequestEncoder; + + fn get_type(&self) -> AttributeType { + AttributeType::new(Self::CODEPOINT) + } +} + +/// [`ChangeRequest`] decoder. +#[derive(Debug, Default)] +pub struct ChangeRequestDecoder(U32beDecoder); + +impl ChangeRequestDecoder { + /// Makes a new `ChangeRequestDecoder` instance. + pub fn new() -> Self { + Self::default() + } +} +impl_decode!(ChangeRequestDecoder, ChangeRequest, |item| { + Ok(ChangeRequest((item & 0x4) != 0, (item & 0x2) != 0)) +}); + +/// [`ChangeRequest`] encoder. +#[derive(Debug, Default)] +pub struct ChangeRequestEncoder(U32beEncoder); + +impl ChangeRequestEncoder { + /// Makes a new `ChangeRequestEncoder` instance. + pub fn new() -> Self { + Self::default() + } +} +impl_encode!(ChangeRequestEncoder, ChangeRequest, |item: Self::Item| { + let ip = item.0 as u8; + let port = item.1 as u8; + ((ip << 1 | port) << 1) as u32 +}); + +pub fn tid_to_u32(tid: &TransactionId) -> u32 { + let mut tid_buf = [0u8; 4]; + // copy bytes from msg_tid to tid_buf + tid_buf[..].copy_from_slice(&tid.as_bytes()[8..12]); + u32::from_le_bytes(tid_buf) +} + +pub fn u32_to_tid(tid: u32) -> TransactionId { + let tid_buf = tid.to_le_bytes(); + let mut tid_arr = [0u8; 12]; + tid_arr[..4].copy_from_slice(&0xdeadbeefu32.to_be_bytes()); + tid_arr[8..12].copy_from_slice(&tid_buf); + TransactionId::new(tid_arr) +} + +define_attribute_enums!( + Attribute, + AttributeDecoder, + AttributeEncoder, + [ + Software, + MappedAddress, + XorMappedAddress, + XorMappedAddress2, + OtherAddress, + ChangeRequest, + ChangedAddress, + SourceAddress, + ResponseOrigin + ] +); diff --git a/rust/easytier/src/connector/direct.rs b/rust/easytier/src/connector/direct.rs new file mode 100644 index 0000000..7dff78c --- /dev/null +++ b/rust/easytier/src/connector/direct.rs @@ -0,0 +1,600 @@ +// try connect peers directly, with either its public ip or lan ip + +use std::{ + net::SocketAddr, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; + +use crate::{ + common::{error::Error, global_ctx::ArcGlobalCtx, PeerId}, + peers::{ + peer_manager::PeerManager, peer_rpc::PeerRpcManager, + peer_rpc_service::DirectConnectorManagerRpcServer, + }, + proto::{ + peer_rpc::{ + DirectConnectorRpc, DirectConnectorRpcClientFactory, DirectConnectorRpcServer, + GetIpListRequest, GetIpListResponse, + }, + rpc_types::controller::BaseController, + }, +}; + +use crate::proto::cli::PeerConnInfo; +use anyhow::Context; +use rand::Rng; +use tokio::{task::JoinSet, time::timeout}; +use tracing::Instrument; +use url::Host; + +use super::create_connector_by_url; + +pub const DIRECT_CONNECTOR_SERVICE_ID: u32 = 1; +pub const DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC: u64 = 300; + +static TESTING: AtomicBool = AtomicBool::new(false); + +#[async_trait::async_trait] +pub trait PeerManagerForDirectConnector { + async fn list_peers(&self) -> Vec; + async fn list_peer_conns(&self, peer_id: PeerId) -> Option>; + fn get_peer_rpc_mgr(&self) -> Arc; +} + +#[async_trait::async_trait] +impl PeerManagerForDirectConnector for PeerManager { + async fn list_peers(&self) -> Vec { + let mut ret = vec![]; + + let routes = self.list_routes().await; + for r in routes + .iter() + .filter(|r| r.feature_flag.map(|r| !r.is_public_server).unwrap_or(true)) + { + ret.push(r.peer_id); + } + + ret + } + + async fn list_peer_conns(&self, peer_id: PeerId) -> Option> { + self.get_peer_map().list_peer_conns(peer_id).await + } + + fn get_peer_rpc_mgr(&self) -> Arc { + self.get_peer_rpc_mgr() + } +} + +#[derive(Hash, Eq, PartialEq, Clone)] +struct DstBlackListItem(PeerId, String); + +#[derive(Hash, Eq, PartialEq, Clone)] +struct DstListenerUrlBlackListItem(PeerId, url::Url); + +struct DirectConnectorManagerData { + global_ctx: ArcGlobalCtx, + peer_manager: Arc, + dst_blacklist: timedmap::TimedMap, + dst_listener_blacklist: timedmap::TimedMap, +} + +impl DirectConnectorManagerData { + pub fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc) -> Self { + Self { + global_ctx, + peer_manager, + dst_blacklist: timedmap::TimedMap::new(), + dst_listener_blacklist: timedmap::TimedMap::new(), + } + } +} + +impl std::fmt::Debug for DirectConnectorManagerData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DirectConnectorManagerData") + .field("peer_manager", &self.peer_manager) + .finish() + } +} + +pub struct DirectConnectorManager { + global_ctx: ArcGlobalCtx, + data: Arc, + + tasks: JoinSet<()>, +} + +impl DirectConnectorManager { + pub fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc) -> Self { + Self { + global_ctx: global_ctx.clone(), + data: Arc::new(DirectConnectorManagerData::new(global_ctx, peer_manager)), + tasks: JoinSet::new(), + } + } + + pub fn run(&mut self) { + if self.global_ctx.get_flags().disable_p2p { + return; + } + + self.run_as_server(); + self.run_as_client(); + } + + pub fn run_as_server(&mut self) { + self.data + .peer_manager + .get_peer_rpc_mgr() + .rpc_server() + .registry() + .register( + DirectConnectorRpcServer::new(DirectConnectorManagerRpcServer::new( + self.global_ctx.clone(), + )), + &self.data.global_ctx.get_network_name(), + ); + } + + pub fn run_as_client(&mut self) { + let data = self.data.clone(); + let my_peer_id = self.data.peer_manager.my_peer_id(); + self.tasks.spawn( + async move { + loop { + let peers = data.peer_manager.list_peers().await; + let mut tasks = JoinSet::new(); + for peer_id in peers { + if peer_id == my_peer_id { + continue; + } + tasks.spawn(Self::do_try_direct_connect(data.clone(), peer_id)); + } + + while let Some(task_ret) = tasks.join_next().await { + tracing::debug!(?task_ret, ?my_peer_id, "direct connect task ret"); + } + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } + } + .instrument( + tracing::info_span!("direct_connector_client", my_id = ?self.global_ctx.id), + ), + ); + } + + async fn do_try_connect_to_ip( + data: Arc, + dst_peer_id: PeerId, + addr: String, + ) -> Result<(), Error> { + data.dst_blacklist.cleanup(); + if data + .dst_blacklist + .contains(&DstBlackListItem(dst_peer_id.clone(), addr.clone())) + { + tracing::debug!("try_connect_to_ip failed, addr in blacklist: {}", addr); + return Err(Error::UrlInBlacklist); + } + + let connector = create_connector_by_url(&addr, &data.global_ctx).await?; + let (peer_id, conn_id) = timeout( + std::time::Duration::from_secs(5), + data.peer_manager.try_connect(connector), + ) + .await??; + + // let (peer_id, conn_id) = data.peer_manager.try_connect(connector).await?; + + if peer_id != dst_peer_id && !TESTING.load(Ordering::Relaxed) { + tracing::info!( + "connect to ip succ: {}, but peer id mismatch, expect: {}, actual: {}", + addr, + dst_peer_id, + peer_id + ); + data.peer_manager + .get_peer_map() + .close_peer_conn(peer_id, &conn_id) + .await?; + return Err(Error::InvalidUrl(addr)); + } + Ok(()) + } + + #[tracing::instrument] + async fn try_connect_to_ip( + data: Arc, + dst_peer_id: PeerId, + addr: String, + ) -> Result<(), Error> { + let mut rand_gen = rand::rngs::OsRng::default(); + let backoff_ms = vec![1000, 2000, 4000]; + let mut backoff_idx = 0; + + loop { + let ret = Self::do_try_connect_to_ip(data.clone(), dst_peer_id, addr.clone()).await; + tracing::debug!(?ret, ?dst_peer_id, ?addr, "try_connect_to_ip return"); + if matches!(ret, Err(Error::UrlInBlacklist) | Ok(_)) { + return ret; + } + + if backoff_idx < backoff_ms.len() { + let delta = backoff_ms[backoff_idx] >> 1; + assert!(delta > 0); + assert!(delta < backoff_ms[backoff_idx]); + + tokio::time::sleep(Duration::from_millis( + (backoff_ms[backoff_idx] + rand_gen.gen_range(-delta..delta)) as u64, + )) + .await; + + backoff_idx += 1; + continue; + } else { + data.dst_blacklist.insert( + DstBlackListItem(dst_peer_id.clone(), addr.clone()), + (), + std::time::Duration::from_secs(DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC), + ); + + return ret; + } + } + } + + #[tracing::instrument] + async fn do_try_direct_connect_internal( + data: Arc, + dst_peer_id: PeerId, + ip_list: GetIpListResponse, + ) -> Result<(), Error> { + data.dst_listener_blacklist.cleanup(); + + let enable_ipv6 = data.global_ctx.get_flags().enable_ipv6; + let available_listeners = ip_list + .listeners + .into_iter() + .map(Into::::into) + .filter_map(|l| if l.scheme() != "ring" { Some(l) } else { None }) + .filter(|l| l.port().is_some() && l.host().is_some()) + .filter(|l| { + !data + .dst_listener_blacklist + .contains(&DstListenerUrlBlackListItem(dst_peer_id.clone(), l.clone())) + }) + .filter(|l| enable_ipv6 || !matches!(l.host().unwrap().to_owned(), Host::Ipv6(_))) + .collect::>(); + + tracing::debug!(?available_listeners, "got available listeners"); + + let mut listener = available_listeners.get(0).ok_or(anyhow::anyhow!( + "peer {} have no valid listener", + dst_peer_id + ))?; + + // if have default listener, use it first + listener = available_listeners + .iter() + .find(|l| l.scheme() == data.global_ctx.get_flags().default_protocol) + .unwrap_or(listener); + + let mut tasks = JoinSet::new(); + + let listener_host = listener.socket_addrs(|| None).unwrap().pop(); + match listener_host { + Some(SocketAddr::V4(s_addr)) => { + if s_addr.ip().is_unspecified() { + ip_list.interface_ipv4s.iter().for_each(|ip| { + let mut addr = (*listener).clone(); + if addr.set_host(Some(ip.to_string().as_str())).is_ok() { + tasks.spawn(Self::try_connect_to_ip( + data.clone(), + dst_peer_id.clone(), + addr.to_string(), + )); + } else { + tracing::error!( + ?ip, + ?listener, + ?dst_peer_id, + "failed to set host for interface ipv4" + ); + } + }); + + if let Some(public_ipv4) = ip_list.public_ipv4 { + let mut addr = (*listener).clone(); + if addr + .set_host(Some(public_ipv4.to_string().as_str())) + .is_ok() + { + tasks.spawn(Self::try_connect_to_ip( + data.clone(), + dst_peer_id.clone(), + addr.to_string(), + )); + } else { + tracing::error!( + ?public_ipv4, + ?listener, + ?dst_peer_id, + "failed to set host for public ipv4" + ); + } + } + } else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) { + tasks.spawn(Self::try_connect_to_ip( + data.clone(), + dst_peer_id.clone(), + listener.to_string(), + )); + } + } + Some(SocketAddr::V6(s_addr)) => { + if s_addr.ip().is_unspecified() { + ip_list.interface_ipv6s.iter().for_each(|ip| { + let mut addr = (*listener).clone(); + if addr + .set_host(Some(format!("[{}]", ip.to_string()).as_str())) + .is_ok() + { + tasks.spawn(Self::try_connect_to_ip( + data.clone(), + dst_peer_id.clone(), + addr.to_string(), + )); + } else { + tracing::error!( + ?ip, + ?listener, + ?dst_peer_id, + "failed to set host for interface ipv6" + ); + } + }); + + if let Some(public_ipv6) = ip_list.public_ipv6 { + let mut addr = (*listener).clone(); + if addr + .set_host(Some(format!("[{}]", public_ipv6.to_string()).as_str())) + .is_ok() + { + tasks.spawn(Self::try_connect_to_ip( + data.clone(), + dst_peer_id.clone(), + addr.to_string(), + )); + } else { + tracing::error!( + ?public_ipv6, + ?listener, + ?dst_peer_id, + "failed to set host for public ipv6" + ); + } + } + } else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) { + tasks.spawn(Self::try_connect_to_ip( + data.clone(), + dst_peer_id.clone(), + listener.to_string(), + )); + } + } + p => { + tracing::error!(?p, ?listener, "failed to parse ip version from listener"); + } + } + + let mut has_succ = false; + while let Some(ret) = tasks.join_next().await { + match ret { + Ok(Ok(_)) => { + has_succ = true; + tracing::info!( + ?dst_peer_id, + ?listener, + "try direct connect to peer success" + ); + break; + } + Ok(Err(e)) => { + tracing::info!(?e, "try direct connect to peer failed"); + } + Err(e) => { + tracing::error!(?e, "try direct connect to peer task join failed"); + } + } + } + + if !has_succ { + data.dst_listener_blacklist.insert( + DstListenerUrlBlackListItem(dst_peer_id.clone(), listener.clone()), + (), + std::time::Duration::from_secs(DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC), + ); + } + + Ok(()) + } + + #[tracing::instrument] + async fn do_try_direct_connect( + data: Arc, + dst_peer_id: PeerId, + ) -> Result<(), Error> { + let peer_manager = data.peer_manager.clone(); + // check if we have direct connection with dst_peer_id + if let Some(c) = peer_manager.list_peer_conns(dst_peer_id).await { + // currently if we have any type of direct connection (udp or tcp), we will not try to connect + if !c.is_empty() { + return Ok(()); + } + } + + tracing::debug!("try direct connect to peer: {}", dst_peer_id); + + let rpc_stub = peer_manager + .get_peer_rpc_mgr() + .rpc_client() + .scoped_client::>( + peer_manager.my_peer_id(), + dst_peer_id, + data.global_ctx.get_network_name(), + ); + + let ip_list = rpc_stub + .get_ip_list(BaseController::default(), GetIpListRequest {}) + .await + .with_context(|| format!("get ip list from peer {}", dst_peer_id))?; + + tracing::info!(ip_list = ?ip_list, dst_peer_id = ?dst_peer_id, "got ip list"); + + Self::do_try_direct_connect_internal(data, dst_peer_id, ip_list).await + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use crate::{ + connector::direct::{ + DirectConnectorManager, DirectConnectorManagerData, DstBlackListItem, + DstListenerUrlBlackListItem, + }, + instance::listeners::ListenerManager, + peers::tests::{ + connect_peer_manager, create_mock_peer_manager, wait_route_appear, + wait_route_appear_with_cost, + }, + proto::peer_rpc::GetIpListResponse, + }; + + use super::TESTING; + + #[tokio::test] + async fn direct_connector_mapped_listener() { + TESTING.store(true, std::sync::atomic::Ordering::Relaxed); + let p_a = create_mock_peer_manager().await; + let p_b = create_mock_peer_manager().await; + let p_c = create_mock_peer_manager().await; + let p_x = create_mock_peer_manager().await; + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_b.clone(), p_c.clone()).await; + connect_peer_manager(p_c.clone(), p_x.clone()).await; + + wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap(); + wait_route_appear(p_a.clone(), p_x.clone()).await.unwrap(); + + let mut f = p_a.get_global_ctx().get_flags(); + f.bind_device = false; + p_a.get_global_ctx().config.set_flags(f); + + p_c.get_global_ctx() + .config + .set_mapped_listeners(Some(vec!["tcp://127.0.0.1:11334".parse().unwrap()])); + + p_x.get_global_ctx() + .config + .set_listeners(vec!["tcp://0.0.0.0:11334".parse().unwrap()]); + let mut lis_x = ListenerManager::new(p_x.get_global_ctx(), p_x.clone()); + lis_x.prepare_listeners().await.unwrap(); + lis_x.run().await.unwrap(); + + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + let mut dm_a = DirectConnectorManager::new(p_a.get_global_ctx(), p_a.clone()); + let mut dm_c = DirectConnectorManager::new(p_c.get_global_ctx(), p_c.clone()); + dm_a.run_as_client(); + dm_c.run_as_server(); + // p_c's mapped listener is p_x's listener, so p_a should connect to p_x directly + + wait_route_appear_with_cost(p_a.clone(), p_x.my_peer_id(), Some(1)) + .await + .unwrap(); + } + + #[rstest::rstest] + #[tokio::test] + async fn direct_connector_basic_test( + #[values("tcp", "udp", "wg")] proto: &str, + #[values("true", "false")] ipv6: bool, + ) { + if ipv6 && proto != "udp" { + return; + } + + let p_a = create_mock_peer_manager().await; + let p_b = create_mock_peer_manager().await; + let p_c = create_mock_peer_manager().await; + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_b.clone(), p_c.clone()).await; + + wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap(); + + let mut dm_a = DirectConnectorManager::new(p_a.get_global_ctx(), p_a.clone()); + let mut dm_c = DirectConnectorManager::new(p_c.get_global_ctx(), p_c.clone()); + + dm_a.run_as_client(); + dm_c.run_as_server(); + + if !ipv6 { + let port = if proto == "wg" { 11040 } else { 11041 }; + p_c.get_global_ctx().config.set_listeners(vec![format!( + "{}://0.0.0.0:{}", + proto, port + ) + .parse() + .unwrap()]); + } + let mut f = p_c.get_global_ctx().config.get_flags(); + f.enable_ipv6 = ipv6; + p_c.get_global_ctx().config.set_flags(f); + let mut lis_c = ListenerManager::new(p_c.get_global_ctx(), p_c.clone()); + lis_c.prepare_listeners().await.unwrap(); + + lis_c.run().await.unwrap(); + + wait_route_appear_with_cost(p_a.clone(), p_c.my_peer_id(), Some(1)) + .await + .unwrap(); + } + + #[tokio::test] + async fn direct_connector_scheme_blacklist() { + let p_a = create_mock_peer_manager().await; + let data = Arc::new(DirectConnectorManagerData::new( + p_a.get_global_ctx(), + p_a.clone(), + )); + let mut ip_list = GetIpListResponse::default(); + ip_list + .listeners + .push("tcp://127.0.0.1:10222".parse().unwrap()); + + ip_list + .interface_ipv4s + .push("127.0.0.1".parse::().unwrap().into()); + + DirectConnectorManager::do_try_direct_connect_internal(data.clone(), 1, ip_list.clone()) + .await + .unwrap(); + + assert!(data + .dst_listener_blacklist + .contains(&DstListenerUrlBlackListItem( + 1, + "tcp://127.0.0.1:10222".parse().unwrap() + ))); + + assert!(data + .dst_blacklist + .contains(&DstBlackListItem(1, ip_list.listeners[0].to_string()))); + } +} diff --git a/rust/easytier/src/connector/dns_connector.rs b/rust/easytier/src/connector/dns_connector.rs new file mode 100644 index 0000000..6d37e60 --- /dev/null +++ b/rust/easytier/src/connector/dns_connector.rs @@ -0,0 +1,265 @@ +use std::{net::SocketAddr, sync::Arc}; + +use crate::{ + common::{error::Error, global_ctx::ArcGlobalCtx}, + tunnel::{Tunnel, TunnelConnector, TunnelError, PROTO_PORT_OFFSET}, +}; +use anyhow::Context; +use dashmap::DashSet; +use hickory_resolver::{ + config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts}, + proto::rr::rdata::SRV, + TokioAsyncResolver, +}; +use rand::{seq::SliceRandom, Rng as _}; + +use crate::proto::common::TunnelInfo; + +use super::{create_connector_by_url, http_connector::TunnelWithInfo}; + +fn weighted_choice(options: &[(T, u64)]) -> Option<&T> { + let total_weight = options.iter().map(|(_, weight)| *weight).sum(); + let mut rng = rand::thread_rng(); + let rand_value = rng.gen_range(0..total_weight); + let mut accumulated_weight = 0; + + for (item, weight) in options { + accumulated_weight += *weight; + if rand_value < accumulated_weight { + return Some(item); + } + } + + None +} + +#[derive(Debug)] +pub struct DNSTunnelConnector { + addr: url::Url, + bind_addrs: Vec, + global_ctx: ArcGlobalCtx, + + default_resolve_config: ResolverConfig, + default_resolve_opts: ResolverOpts, +} + +impl DNSTunnelConnector { + pub fn new(addr: url::Url, global_ctx: ArcGlobalCtx) -> Self { + let mut default_resolve_config = ResolverConfig::new(); + default_resolve_config.add_name_server(NameServerConfig::new( + "223.5.5.5:53".parse().unwrap(), + Protocol::Udp, + )); + default_resolve_config.add_name_server(NameServerConfig::new( + "180.184.1.1:53".parse().unwrap(), + Protocol::Udp, + )); + Self { + addr, + bind_addrs: Vec::new(), + global_ctx, + + default_resolve_config, + default_resolve_opts: ResolverOpts::default(), + } + } + + #[tracing::instrument(ret, err)] + pub async fn handle_txt_record( + &self, + domain_name: &str, + ) -> Result, Error> { + let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap_or( + TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()), + ); + + let response = resolver.txt_lookup(domain_name).await.with_context(|| { + format!( + "txt_lookup failed, domain_name: {}", + domain_name.to_string() + ) + })?; + + let txt_record = response.iter().next().with_context(|| { + format!( + "no txt record found, domain_name: {}", + domain_name.to_string() + ) + })?; + + let txt_data = String::from_utf8_lossy(&txt_record.txt_data()[0]); + tracing::info!(?txt_data, ?domain_name, "get txt record"); + + let candidate_urls = txt_data + .split(" ") + .map(|s| s.to_string()) + .filter_map(|s| url::Url::parse(s.as_str()).ok()) + .collect::>(); + + // shuffle candidate_urls and get the first one + let url = candidate_urls + .choose(&mut rand::thread_rng()) + .with_context(|| { + format!( + "no valid url found, txt_data: {}, expecting an url list splitted by space", + txt_data + ) + })?; + + let connector = create_connector_by_url(url.as_str(), &self.global_ctx).await; + + connector + } + + fn handle_one_srv_record(record: &SRV, protocol: &str) -> Result<(url::Url, u64), Error> { + // port must be non-zero + if record.port() == 0 { + return Err(anyhow::anyhow!("port must be non-zero").into()); + } + + let connector_dst = record.target().to_utf8(); + let dst_url = format!("{}://{}:{}", protocol, connector_dst, record.port()); + + Ok(( + dst_url.parse().with_context(|| { + format!( + "parse dst_url failed, protocol: {}, connector_dst: {}, port: {}, dst_url: {}", + protocol, + connector_dst, + record.port(), + dst_url + ) + })?, + record.priority() as _, + )) + } + + #[tracing::instrument(ret, err)] + pub async fn handle_srv_record( + &self, + domain_name: &str, + ) -> Result, Error> { + tracing::info!("handle_srv_record: {}", domain_name); + + let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap_or( + TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()), + ); + + let srv_domains = PROTO_PORT_OFFSET + .iter() + .map(|(p, _)| (format!("_easytier._{}.{}", p, domain_name), *p)) // _easytier._udp.{domain_name} + .collect::>(); + tracing::info!("build srv_domains: {:?}", srv_domains); + let responses = Arc::new(DashSet::new()); + let srv_lookup_tasks = srv_domains + .iter() + .map(|(srv_domain, protocol)| { + let resolver = resolver.clone(); + let responses = responses.clone(); + async move { + let response = resolver.srv_lookup(srv_domain).await.with_context(|| { + format!("srv_lookup failed, srv_domain: {}", srv_domain.to_string()) + })?; + tracing::info!(?response, ?srv_domain, "srv_lookup response"); + for record in response.iter() { + let parsed_record = Self::handle_one_srv_record(record, &protocol); + tracing::info!(?parsed_record, ?srv_domain, "parsed_record"); + if parsed_record.is_err() { + eprintln!( + "got invalid srv record {:?}", + parsed_record.as_ref().unwrap_err() + ); + continue; + } + responses.insert(parsed_record.unwrap()); + } + Ok::<_, Error>(()) + } + }) + .collect::>(); + let _ = futures::future::join_all(srv_lookup_tasks).await; + + let srv_records = responses.iter().map(|r| r.clone()).collect::>(); + if srv_records.is_empty() { + return Err(anyhow::anyhow!("no srv record found").into()); + } + + let url = weighted_choice(srv_records.as_slice()).with_context(|| { + format!( + "failed to choose a srv record, domain_name: {}, srv_records: {:?}", + domain_name.to_string(), + srv_records + ) + })?; + + let connector = create_connector_by_url(url.as_str(), &self.global_ctx).await; + connector + } +} + +#[async_trait::async_trait] +impl super::TunnelConnector for DNSTunnelConnector { + async fn connect(&mut self) -> Result, TunnelError> { + let mut conn = if self.addr.scheme() == "txt" { + self.handle_txt_record(self.addr.host_str().as_ref().unwrap()) + .await + .with_context(|| "get txt record url failed")? + } else if self.addr.scheme() == "srv" { + self.handle_srv_record(self.addr.host_str().as_ref().unwrap()) + .await + .with_context(|| "get srv record url failed")? + } else { + return Err(anyhow::anyhow!( + "unsupported dns scheme: {}, expecting txt or srv", + self.addr.scheme() + ) + .into()); + }; + let t = conn.connect().await?; + let info = t.info().unwrap_or_default(); + Ok(Box::new(TunnelWithInfo::new( + t, + TunnelInfo { + local_addr: info.local_addr.clone(), + remote_addr: Some(self.addr.clone().into()), + tunnel_type: format!( + "{}-{}", + self.addr.scheme(), + info.remote_addr.unwrap_or_default() + ), + }, + ))) + } + + fn remote_url(&self) -> url::Url { + self.addr.clone() + } + + fn set_bind_addrs(&mut self, addrs: Vec) { + self.bind_addrs = addrs; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::global_ctx::tests::get_mock_global_ctx; + + #[tokio::test] + async fn test_txt() { + let url = "txt://txt.easytier.cn"; + let global_ctx = get_mock_global_ctx(); + let mut connector = DNSTunnelConnector::new(url.parse().unwrap(), global_ctx); + let ret = connector.connect().await.unwrap(); + println!("{:?}", ret.info()); + } + + #[tokio::test] + async fn test_srv() { + let url = "srv://easytier.cn"; + let global_ctx = get_mock_global_ctx(); + let mut connector = DNSTunnelConnector::new(url.parse().unwrap(), global_ctx); + let ret = connector.connect().await.unwrap(); + println!("{:?}", ret.info()); + } +} diff --git a/rust/easytier/src/connector/http_connector.rs b/rust/easytier/src/connector/http_connector.rs new file mode 100644 index 0000000..3757e3b --- /dev/null +++ b/rust/easytier/src/connector/http_connector.rs @@ -0,0 +1,309 @@ +use std::{ + net::SocketAddr, + pin::Pin, + sync::{Arc, RwLock}, +}; + +use anyhow::Context; +use http_req::request::{RedirectPolicy, Request}; +use rand::seq::SliceRandom as _; +use url::Url; + +use crate::{ + common::{error::Error, global_ctx::ArcGlobalCtx}, + tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, ZCPacketSink, ZCPacketStream}, +}; + +use crate::proto::common::TunnelInfo; + +use super::create_connector_by_url; + +pub struct TunnelWithInfo { + inner: Box, + info: TunnelInfo, +} + +impl TunnelWithInfo { + pub fn new(inner: Box, info: TunnelInfo) -> Self { + Self { inner, info } + } +} + +impl Tunnel for TunnelWithInfo { + fn split(&self) -> (Pin>, Pin>) { + self.inner.split() + } + + fn info(&self) -> Option { + Some(self.info.clone()) + } +} + +#[derive(Debug, PartialEq, Copy, Clone)] +enum HttpRedirectType { + Unknown, + // redirected url is in the path of new url + RedirectToQuery, + // redirected url is the entire new url + RedirectToUrl, + // redirected url is in the body of response + BodyUrls, +} + +#[derive(Debug)] +pub struct HttpTunnelConnector { + addr: url::Url, + bind_addrs: Vec, + ip_version: IpVersion, + global_ctx: ArcGlobalCtx, + redirect_type: HttpRedirectType, +} + +impl HttpTunnelConnector { + pub fn new(addr: url::Url, global_ctx: ArcGlobalCtx) -> Self { + Self { + addr, + bind_addrs: Vec::new(), + ip_version: IpVersion::Both, + global_ctx, + redirect_type: HttpRedirectType::Unknown, + } + } + + #[tracing::instrument(ret)] + async fn handle_302_redirect( + &mut self, + new_url: url::Url, + url_str: &str, + ) -> Result, Error> { + // the url should be in following format: + // 1: http(s)://easytier.cn/?url=tcp://10.147.22.22:11010 (scheme is http, domain is ignored, path is splitted into proto type and addr) + // 2: http(s)://tcp://10.137.22.22:11010 (connector url is appended to the scheme) + // 3: tcp://10.137.22.22:11010 (scheme is protocol type, the url is used to construct a connector directly) + tracing::info!("redirect to {}", new_url); + let url = url::Url::parse(new_url.as_str()) + .with_context(|| format!("parsing redirect url failed. url: {}", new_url))?; + if url.scheme() == "http" || url.scheme() == "https" { + let mut query = new_url + .query_pairs() + .filter_map(|x| url::Url::parse(&x.1).ok()) + .collect::>(); + query.shuffle(&mut rand::thread_rng()); + if !query.is_empty() { + tracing::info!("try to create connector by url: {}", query[0]); + self.redirect_type = HttpRedirectType::RedirectToQuery; + return create_connector_by_url(&query[0].to_string(), &self.global_ctx).await; + } else if let Some(new_url) = url_str + .strip_prefix(format!("{}://", url.scheme()).as_str()) + .and_then(|x| Url::parse(x).ok()) + { + // stripe the scheme and create connector by url + self.redirect_type = HttpRedirectType::RedirectToUrl; + return create_connector_by_url(new_url.as_str(), &self.global_ctx).await; + } + return Err(Error::InvalidUrl(format!( + "no valid connector url found in url: {}", + url + ))); + } else { + self.redirect_type = HttpRedirectType::RedirectToUrl; + return create_connector_by_url(new_url.as_str(), &self.global_ctx).await; + } + } + + #[tracing::instrument] + async fn handle_200_success( + &mut self, + body: &String, + ) -> Result, Error> { + // resp body should be line of connector urls, like: + // tcp://10.1.1.1:11010 + // udp://10.1.1.1:11010 + let mut lines = body + .lines() + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + .collect::>(); + + tracing::info!("get {} lines of connector urls", lines.len()); + + // shuffle the lines and pick the usable one + lines.shuffle(&mut rand::thread_rng()); + + for line in lines { + let url = url::Url::parse(line); + if url.is_err() { + tracing::warn!("invalid url: {}, skip it", line); + continue; + } + self.redirect_type = HttpRedirectType::BodyUrls; + return create_connector_by_url(line, &self.global_ctx).await; + } + + Err(Error::InvalidUrl(format!( + "no valid connector url found, response body: {}", + body + ))) + } + + #[tracing::instrument(ret)] + pub async fn get_redirected_connector( + &mut self, + original_url: &str, + ) -> Result, Error> { + self.redirect_type = HttpRedirectType::Unknown; + tracing::info!("get_redirected_url: {}", original_url); + // Container for body of a response. + let body = Arc::new(RwLock::new(Vec::new())); + + let original_url_clone = original_url.to_string(); + let body_clone = body.clone(); + let res = tokio::task::spawn_blocking(move || { + let uri = http_req::uri::Uri::try_from(original_url_clone.as_ref()) + .with_context(|| format!("parsing url failed. url: {}", original_url_clone))?; + + tracing::info!("sending http request to {}", uri); + + Request::new(&uri) + .redirect_policy(RedirectPolicy::Limit(0)) + .timeout(std::time::Duration::from_secs(20)) + .send(&mut *body_clone.write().unwrap()) + .with_context(|| format!("sending http request failed. url: {}", uri)) + }) + .await + .map_err(|e| Error::InvalidUrl(format!("task join error: {}", e)))??; + + let body = String::from_utf8_lossy(&body.read().unwrap()).to_string(); + + if res.status_code().is_redirect() { + let redirect_url = res + .headers() + .get("Location") + .ok_or_else(|| Error::InvalidUrl("no redirect address found".to_string()))?; + let new_url = url::Url::parse(redirect_url.as_str()) + .with_context(|| format!("parsing redirect url failed. url: {}", redirect_url))?; + return self.handle_302_redirect(new_url, &redirect_url).await; + } else if res.status_code().is_success() { + return self.handle_200_success(&body).await; + } else { + return Err(Error::InvalidUrl(format!( + "unexpected response, resp: {:?}, body: {}", + res, body, + ))); + } + } +} + +#[async_trait::async_trait] +impl super::TunnelConnector for HttpTunnelConnector { + async fn connect(&mut self) -> Result, TunnelError> { + let mut conn = self + .get_redirected_connector(self.addr.to_string().as_str()) + .await + .with_context(|| "get redirected url failed")?; + conn.set_ip_version(self.ip_version); + let t = conn.connect().await?; + let info = t.info().unwrap_or_default(); + Ok(Box::new(TunnelWithInfo::new( + t, + TunnelInfo { + local_addr: info.local_addr.clone(), + remote_addr: Some(self.addr.clone().into()), + tunnel_type: format!( + "{:?}-{}", + self.redirect_type, + info.remote_addr.unwrap_or_default() + ), + }, + ))) + } + + fn remote_url(&self) -> url::Url { + self.addr.clone() + } + + fn set_bind_addrs(&mut self, addrs: Vec) { + self.bind_addrs = addrs; + } + + fn set_ip_version(&mut self, ip_version: IpVersion) { + self.ip_version = ip_version; + } +} + +#[cfg(test)] +mod tests { + use tokio::{io::AsyncWriteExt as _, net::TcpListener}; + + use crate::{ + common::global_ctx::tests::get_mock_global_ctx, + tunnel::{tcp::TcpTunnelListener, TunnelConnector, TunnelListener}, + }; + + use super::*; + + async fn run_http_redirect_server(port: u16, test_type: HttpRedirectType) -> Result<(), Error> { + let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?; + let (mut stream, _) = listener.accept().await?; + + match test_type { + HttpRedirectType::RedirectToQuery => { + let resp = "HTTP/1.1 301 Moved Permanently\r\nLocation: http://test.com/?url=tcp://127.0.0.1:25888\r\n\r\n"; + stream.write_all(resp.as_bytes()).await?; + } + HttpRedirectType::RedirectToUrl => { + let resp = + "HTTP/1.1 301 Moved Permanently\r\nLocation: tcp://127.0.0.1:25888\r\n\r\n"; + stream.write_all(resp.as_bytes()).await?; + } + HttpRedirectType::BodyUrls => { + let resp = "HTTP/1.1 200 OK\r\n\r\ntcp://127.0.0.1:25888"; + stream.write_all(resp.as_bytes()).await?; + } + HttpRedirectType::Unknown => { + panic!("unexpected test type"); + } + } + + Ok(()) + } + + #[rstest::rstest] + #[serial_test::serial(http_redirect_test)] + #[tokio::test] + async fn http_redirect_test( + // 1. 301 redirect + // 2. 200 success with valid connector urls + #[values( + HttpRedirectType::RedirectToQuery, + HttpRedirectType::RedirectToUrl, + HttpRedirectType::BodyUrls + )] + test_type: HttpRedirectType, + ) { + let http_task = tokio::spawn(run_http_redirect_server(35888, test_type)); + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + let test_url: url::Url = "http://127.0.0.1:35888".parse().unwrap(); + let global_ctx = get_mock_global_ctx(); + let mut flags = global_ctx.config.get_flags(); + flags.bind_device = false; + global_ctx.config.set_flags(flags); + let mut connector = HttpTunnelConnector::new(test_url.clone(), global_ctx.clone()); + + let mut listener = TcpTunnelListener::new("tcp://0.0.0.0:25888".parse().unwrap()); + listener.listen().await.unwrap(); + + let task = tokio::spawn(async move { + let _conn = listener.accept().await.unwrap(); + }); + + let t = connector.connect().await.unwrap(); + assert_eq!(connector.redirect_type, test_type); + let info = t.info().unwrap(); + let remote_addr = info.remote_addr.unwrap(); + assert_eq!(remote_addr, test_url.into()); + + tokio::join!(task).0.unwrap(); + tokio::join!(http_task).0.unwrap().unwrap(); + } +} diff --git a/rust/easytier/src/connector/manual.rs b/rust/easytier/src/connector/manual.rs new file mode 100644 index 0000000..4181cfa --- /dev/null +++ b/rust/easytier/src/connector/manual.rs @@ -0,0 +1,478 @@ +use std::{collections::BTreeSet, sync::Arc}; + +use anyhow::Context; +use dashmap::{DashMap, DashSet}; +use tokio::{ + sync::{broadcast::Receiver, mpsc, Mutex}, + task::JoinSet, + time::timeout, +}; + +use crate::{ + common::PeerId, + peers::peer_conn::PeerConnId, + proto::{ + cli::{ + ConnectorManageAction, ListConnectorResponse, ManageConnectorResponse, PeerConnInfo, + }, + rpc_types::{self, controller::BaseController}, + }, + tunnel::{IpVersion, TunnelConnector}, +}; + +use crate::{ + common::{ + error::Error, + global_ctx::{ArcGlobalCtx, GlobalCtxEvent}, + netns::NetNS, + }, + connector::set_bind_addr_for_peer_connector, + peers::peer_manager::PeerManager, + proto::cli::{ + Connector, ConnectorManageRpc, ConnectorStatus, ListConnectorRequest, + ManageConnectorRequest, + }, + use_global_var, +}; + +use super::create_connector_by_url; + +type MutexConnector = Arc>>; +type ConnectorMap = Arc>; + +#[derive(Debug, Clone)] +struct ReconnResult { + dead_url: String, + peer_id: PeerId, + conn_id: PeerConnId, +} + +struct ConnectorManagerData { + connectors: ConnectorMap, + reconnecting: DashSet, + peer_manager: Arc, + alive_conn_urls: Arc>, + // user removed connector urls + removed_conn_urls: Arc>, + net_ns: NetNS, + global_ctx: ArcGlobalCtx, +} + +pub struct ManualConnectorManager { + global_ctx: ArcGlobalCtx, + data: Arc, + tasks: JoinSet<()>, +} + +impl ManualConnectorManager { + pub fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc) -> Self { + let connectors = Arc::new(DashMap::new()); + let tasks = JoinSet::new(); + let event_subscriber = global_ctx.subscribe(); + + let mut ret = Self { + global_ctx: global_ctx.clone(), + data: Arc::new(ConnectorManagerData { + connectors, + reconnecting: DashSet::new(), + peer_manager, + alive_conn_urls: Arc::new(DashSet::new()), + removed_conn_urls: Arc::new(DashSet::new()), + net_ns: global_ctx.net_ns.clone(), + global_ctx, + }), + tasks, + }; + + ret.tasks + .spawn(Self::conn_mgr_reconn_routine(ret.data.clone())); + ret.tasks.spawn(Self::conn_mgr_handle_event_routine( + ret.data.clone(), + event_subscriber, + )); + + ret + } + + pub fn add_connector(&self, connector: T) + where + T: TunnelConnector + 'static, + { + tracing::info!("add_connector: {}", connector.remote_url()); + self.data.connectors.insert( + connector.remote_url().into(), + Arc::new(Mutex::new(Box::new(connector))), + ); + } + + pub async fn add_connector_by_url(&self, url: &str) -> Result<(), Error> { + self.add_connector(create_connector_by_url(url, &self.global_ctx).await?); + Ok(()) + } + + pub async fn remove_connector(&self, url: url::Url) -> Result<(), Error> { + tracing::info!("remove_connector: {}", url); + let url = url.into(); + if !self + .list_connectors() + .await + .iter() + .any(|x| x.url.as_ref() == Some(&url)) + { + return Err(Error::NotFound); + } + self.data.removed_conn_urls.insert(url.to_string()); + Ok(()) + } + + pub async fn list_connectors(&self) -> Vec { + let conn_urls: BTreeSet = self + .data + .connectors + .iter() + .map(|x| x.key().clone().into()) + .collect(); + + let dead_urls: BTreeSet = Self::collect_dead_conns(self.data.clone()) + .await + .into_iter() + .collect(); + + let mut ret = Vec::new(); + + for conn_url in conn_urls { + let mut status = ConnectorStatus::Connected; + if dead_urls.contains(&conn_url) { + status = ConnectorStatus::Disconnected; + } + ret.insert( + 0, + Connector { + url: Some(conn_url.parse().unwrap()), + status: status.into(), + }, + ); + } + + let reconnecting_urls: BTreeSet = self + .data + .reconnecting + .iter() + .map(|x| x.clone().into()) + .collect(); + + for conn_url in reconnecting_urls { + ret.insert( + 0, + Connector { + url: Some(conn_url.parse().unwrap()), + status: ConnectorStatus::Connecting.into(), + }, + ); + } + + ret + } + + async fn conn_mgr_handle_event_routine( + data: Arc, + mut event_recv: Receiver, + ) { + loop { + let event = event_recv.recv().await.expect("event_recv got error"); + Self::handle_event(&event, &data).await; + } + } + + async fn conn_mgr_reconn_routine(data: Arc) { + tracing::warn!("conn_mgr_routine started"); + let mut reconn_interval = tokio::time::interval(std::time::Duration::from_millis( + use_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS), + )); + let (reconn_result_send, mut reconn_result_recv) = mpsc::channel(100); + + loop { + tokio::select! { + _ = reconn_interval.tick() => { + let dead_urls = Self::collect_dead_conns(data.clone()).await; + if dead_urls.is_empty() { + continue; + } + for dead_url in dead_urls { + let data_clone = data.clone(); + let sender = reconn_result_send.clone(); + let (_, connector) = data.connectors.remove(&dead_url).unwrap(); + let insert_succ = data.reconnecting.insert(dead_url.clone()); + assert!(insert_succ); + + tokio::spawn(async move { + let reconn_ret = Self::conn_reconnect(data_clone.clone(), dead_url.clone(), connector.clone()).await; + sender.send(reconn_ret).await.unwrap(); + + data_clone.reconnecting.remove(&dead_url).unwrap(); + data_clone.connectors.insert(dead_url.clone(), connector); + }); + } + tracing::info!("reconn_interval tick, done"); + } + + ret = reconn_result_recv.recv() => { + tracing::warn!("reconn_tasks done, reconn result: {:?}", ret); + } + } + } + } + + async fn handle_event(event: &GlobalCtxEvent, data: &ConnectorManagerData) { + let need_add_alive = |conn_info: &PeerConnInfo| conn_info.is_client; + match event { + GlobalCtxEvent::PeerConnAdded(conn_info) => { + if !need_add_alive(conn_info) { + return; + } + let addr = conn_info.tunnel.as_ref().unwrap().remote_addr.clone(); + data.alive_conn_urls.insert(addr.unwrap().to_string()); + tracing::warn!("peer conn added: {:?}", conn_info); + } + + GlobalCtxEvent::PeerConnRemoved(conn_info) => { + if !need_add_alive(conn_info) { + return; + } + let addr = conn_info.tunnel.as_ref().unwrap().remote_addr.clone(); + data.alive_conn_urls.remove(&addr.unwrap().to_string()); + tracing::warn!("peer conn removed: {:?}", conn_info); + } + + _ => {} + } + } + + fn handle_remove_connector(data: Arc) { + let remove_later = DashSet::new(); + for it in data.removed_conn_urls.iter() { + let url = it.key(); + if let Some(_) = data.connectors.remove(url) { + tracing::warn!("connector: {}, removed", url); + continue; + } else if data.reconnecting.contains(url) { + tracing::warn!("connector: {}, reconnecting, remove later.", url); + remove_later.insert(url.clone()); + continue; + } else { + tracing::warn!("connector: {}, not found", url); + } + } + data.removed_conn_urls.clear(); + for it in remove_later.iter() { + data.removed_conn_urls.insert(it.key().clone()); + } + } + + async fn collect_dead_conns(data: Arc) -> BTreeSet { + Self::handle_remove_connector(data.clone()); + + let all_urls: BTreeSet = data + .connectors + .iter() + .map(|x| x.key().clone().into()) + .collect(); + let mut ret = BTreeSet::new(); + for url in all_urls.iter() { + if !data.alive_conn_urls.contains(url) { + ret.insert(url.clone()); + } + } + ret + } + + async fn conn_reconnect_with_ip_version( + data: Arc, + dead_url: String, + connector: MutexConnector, + ip_version: IpVersion, + ) -> Result { + let ip_collector = data.global_ctx.get_ip_collector(); + let net_ns = data.net_ns.clone(); + + connector.lock().await.set_ip_version(ip_version); + + if data.global_ctx.config.get_flags().bind_device { + set_bind_addr_for_peer_connector( + connector.lock().await.as_mut(), + ip_version == IpVersion::V4, + &ip_collector, + ) + .await; + } + + data.global_ctx.issue_event(GlobalCtxEvent::Connecting( + connector.lock().await.remote_url().clone(), + )); + + let _g = net_ns.guard(); + tracing::info!("reconnect try connect... conn: {:?}", connector); + let tunnel = connector.lock().await.connect().await?; + tracing::info!("reconnect get tunnel succ: {:?}", tunnel); + assert_eq!( + dead_url, + tunnel.info().unwrap().remote_addr.unwrap().to_string(), + "info: {:?}", + tunnel.info() + ); + let (peer_id, conn_id) = data.peer_manager.add_client_tunnel(tunnel).await?; + tracing::info!("reconnect succ: {} {} {}", peer_id, conn_id, dead_url); + Ok(ReconnResult { + dead_url, + peer_id, + conn_id, + }) + } + + async fn conn_reconnect( + data: Arc, + dead_url: String, + connector: MutexConnector, + ) -> Result { + tracing::info!("reconnect: {}", dead_url); + + let mut ip_versions = vec![]; + let u = url::Url::parse(&dead_url) + .with_context(|| format!("failed to parse connector url {:?}", dead_url))?; + if u.scheme() == "ring" || u.scheme() == "txt" || u.scheme() == "srv" { + ip_versions.push(IpVersion::Both); + } else { + let addrs = u.socket_addrs(|| Some(1000))?; + tracing::info!(?addrs, ?dead_url, "get ip from url done"); + let mut has_ipv4 = false; + let mut has_ipv6 = false; + for addr in addrs { + if addr.is_ipv4() { + if !has_ipv4 { + ip_versions.insert(0, IpVersion::V4); + } + has_ipv4 = true; + } else if addr.is_ipv6() { + if !has_ipv6 { + ip_versions.push(IpVersion::V6); + } + has_ipv6 = true; + } + } + } + + let mut reconn_ret = Err(Error::AnyhowError(anyhow::anyhow!( + "cannot get ip from url" + ))); + for ip_version in ip_versions { + let use_long_timeout = dead_url.starts_with("http") + || dead_url.starts_with("srv") + || dead_url.starts_with("txt"); + let ret = timeout( + // allow http connector to wait longer + std::time::Duration::from_secs(if use_long_timeout { 20 } else { 2 }), + Self::conn_reconnect_with_ip_version( + data.clone(), + dead_url.clone(), + connector.clone(), + ip_version, + ), + ) + .await; + tracing::info!("reconnect: {} done, ret: {:?}", dead_url, ret); + + if ret.is_ok() && ret.as_ref().unwrap().is_ok() { + reconn_ret = ret.unwrap(); + break; + } else { + if ret.is_err() { + reconn_ret = Err(ret.unwrap_err().into()); + } else if ret.as_ref().unwrap().is_err() { + reconn_ret = Err(ret.unwrap().unwrap_err()); + } + data.global_ctx.issue_event(GlobalCtxEvent::ConnectError( + dead_url.clone(), + format!("{:?}", ip_version), + format!("{:?}", reconn_ret), + )); + } + } + + reconn_ret + } +} + +#[derive(Clone)] +pub struct ConnectorManagerRpcService(pub Arc); + +#[async_trait::async_trait] +impl ConnectorManageRpc for ConnectorManagerRpcService { + type Controller = BaseController; + + async fn list_connector( + &self, + _: BaseController, + _request: ListConnectorRequest, + ) -> Result { + let mut ret = ListConnectorResponse::default(); + let connectors = self.0.list_connectors().await; + ret.connectors = connectors; + Ok(ret) + } + + async fn manage_connector( + &self, + _: BaseController, + req: ManageConnectorRequest, + ) -> Result { + let url: url::Url = req.url.ok_or(anyhow::anyhow!("url is empty"))?.into(); + if req.action == ConnectorManageAction::Remove as i32 { + self.0 + .remove_connector(url.clone()) + .await + .with_context(|| format!("remove connector failed: {:?}", url))?; + return Ok(ManageConnectorResponse::default()); + } else { + self.0 + .add_connector_by_url(url.as_str()) + .await + .with_context(|| format!("add connector failed: {:?}", url))?; + } + Ok(ManageConnectorResponse::default()) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + peers::tests::create_mock_peer_manager, + set_global_var, + tunnel::{Tunnel, TunnelError}, + }; + + use super::*; + + #[tokio::test] + async fn test_reconnect_with_connecting_addr() { + set_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS, 1); + + let peer_mgr = create_mock_peer_manager().await; + let mgr = ManualConnectorManager::new(peer_mgr.get_global_ctx(), peer_mgr); + + struct MockConnector {} + #[async_trait::async_trait] + impl TunnelConnector for MockConnector { + fn remote_url(&self) -> url::Url { + url::Url::parse("tcp://aa.com").unwrap() + } + async fn connect(&mut self) -> Result, TunnelError> { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + Err(TunnelError::InvalidPacket("fake error".into())) + } + } + + mgr.add_connector(MockConnector {}); + + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } +} diff --git a/rust/easytier/src/connector/mod.rs b/rust/easytier/src/connector/mod.rs new file mode 100644 index 0000000..f07afbc --- /dev/null +++ b/rust/easytier/src/connector/mod.rs @@ -0,0 +1,152 @@ +use std::{ + net::{SocketAddr, SocketAddrV4, SocketAddrV6}, + sync::Arc, +}; + +use http_connector::HttpTunnelConnector; + +#[cfg(feature = "quic")] +use crate::tunnel::quic::QUICTunnelConnector; +#[cfg(feature = "wireguard")] +use crate::tunnel::wireguard::{WgConfig, WgTunnelConnector}; +use crate::{ + common::{error::Error, global_ctx::ArcGlobalCtx, network::IPCollector}, + tunnel::{ + check_scheme_and_get_socket_addr, ring::RingTunnelConnector, tcp::TcpTunnelConnector, + udp::UdpTunnelConnector, TunnelConnector, + }, +}; + +pub mod direct; +pub mod manual; +pub mod udp_hole_punch; + +pub mod dns_connector; +pub mod http_connector; + +async fn set_bind_addr_for_peer_connector( + connector: &mut (impl TunnelConnector + ?Sized), + is_ipv4: bool, + ip_collector: &Arc, +) { + if cfg!(target_os = "android") { + return; + } + + let ips = ip_collector.collect_ip_addrs().await; + if is_ipv4 { + let mut bind_addrs = vec![]; + for ipv4 in ips.interface_ipv4s { + let socket_addr = SocketAddrV4::new(ipv4.into(), 0).into(); + bind_addrs.push(socket_addr); + } + connector.set_bind_addrs(bind_addrs); + } else { + let mut bind_addrs = vec![]; + for ipv6 in ips.interface_ipv6s { + let socket_addr = SocketAddrV6::new(ipv6.into(), 0, 0, 0).into(); + bind_addrs.push(socket_addr); + } + connector.set_bind_addrs(bind_addrs); + } + let _ = connector; +} + +pub async fn create_connector_by_url( + url: &str, + global_ctx: &ArcGlobalCtx, +) -> Result, Error> { + let url = url::Url::parse(url).map_err(|_| Error::InvalidUrl(url.to_owned()))?; + match url.scheme() { + "tcp" => { + let dst_addr = check_scheme_and_get_socket_addr::(&url, "tcp")?; + let mut connector = TcpTunnelConnector::new(url); + if global_ctx.config.get_flags().bind_device { + set_bind_addr_for_peer_connector( + &mut connector, + dst_addr.is_ipv4(), + &global_ctx.get_ip_collector(), + ) + .await; + } + return Ok(Box::new(connector)); + } + "udp" => { + let dst_addr = check_scheme_and_get_socket_addr::(&url, "udp")?; + let mut connector = UdpTunnelConnector::new(url); + if global_ctx.config.get_flags().bind_device { + set_bind_addr_for_peer_connector( + &mut connector, + dst_addr.is_ipv4(), + &global_ctx.get_ip_collector(), + ) + .await; + } + return Ok(Box::new(connector)); + } + "http" | "https" => { + let connector = HttpTunnelConnector::new(url, global_ctx.clone()); + return Ok(Box::new(connector)); + } + "ring" => { + check_scheme_and_get_socket_addr::(&url, "ring")?; + let connector = RingTunnelConnector::new(url); + return Ok(Box::new(connector)); + } + #[cfg(feature = "quic")] + "quic" => { + let dst_addr = check_scheme_and_get_socket_addr::(&url, "quic")?; + let mut connector = QUICTunnelConnector::new(url); + if global_ctx.config.get_flags().bind_device { + set_bind_addr_for_peer_connector( + &mut connector, + dst_addr.is_ipv4(), + &global_ctx.get_ip_collector(), + ) + .await; + } + return Ok(Box::new(connector)); + } + #[cfg(feature = "wireguard")] + "wg" => { + let dst_addr = check_scheme_and_get_socket_addr::(&url, "wg")?; + let nid = global_ctx.get_network_identity(); + let wg_config = WgConfig::new_from_network_identity( + &nid.network_name, + &nid.network_secret.unwrap_or_default(), + ); + let mut connector = WgTunnelConnector::new(url, wg_config); + if global_ctx.config.get_flags().bind_device { + set_bind_addr_for_peer_connector( + &mut connector, + dst_addr.is_ipv4(), + &global_ctx.get_ip_collector(), + ) + .await; + } + return Ok(Box::new(connector)); + } + #[cfg(feature = "websocket")] + "ws" | "wss" => { + use crate::tunnel::{FromUrl, IpVersion}; + let dst_addr = SocketAddr::from_url(url.clone(), IpVersion::Both)?; + let mut connector = crate::tunnel::websocket::WSTunnelConnector::new(url); + if global_ctx.config.get_flags().bind_device { + set_bind_addr_for_peer_connector( + &mut connector, + dst_addr.is_ipv4(), + &global_ctx.get_ip_collector(), + ) + .await; + } + return Ok(Box::new(connector)); + } + "txt" | "srv" => { + let connector = dns_connector::DNSTunnelConnector::new(url, global_ctx.clone()); + return Ok(Box::new(connector)); + } + _ => { + return Err(Error::InvalidUrl(url.into())); + } + } +} diff --git a/rust/easytier/src/connector/udp_hole_punch/both_easy_sym.rs b/rust/easytier/src/connector/udp_hole_punch/both_easy_sym.rs new file mode 100644 index 0000000..fc84406 --- /dev/null +++ b/rust/easytier/src/connector/udp_hole_punch/both_easy_sym.rs @@ -0,0 +1,399 @@ +use std::{ + net::{IpAddr, SocketAddr, SocketAddrV4}, + sync::Arc, + time::{Duration, Instant}, +}; + +use anyhow::Context; +use tokio::sync::Mutex; + +use crate::{ + common::{scoped_task::ScopedTask, stun::StunInfoCollectorTrait, PeerId}, + connector::udp_hole_punch::common::{ + try_connect_with_socket, UdpHolePunchListener, HOLE_PUNCH_PACKET_BODY_LEN, + }, + peers::peer_manager::PeerManager, + proto::{ + peer_rpc::{ + SendPunchPacketBothEasySymRequest, SendPunchPacketBothEasySymResponse, + UdpHolePunchRpcClientFactory, + }, + rpc_types::{self, controller::BaseController}, + }, + tunnel::{udp::new_hole_punch_packet, Tunnel}, +}; + +use super::common::{PunchHoleServerCommon, UdpNatType, UdpSocketArray}; + +const UDP_ARRAY_SIZE_FOR_BOTH_EASY_SYM: usize = 25; +const DST_PORT_OFFSET: u16 = 20; +const REMOTE_WAIT_TIME_MS: u64 = 5000; + +pub(crate) struct PunchBothEasySymHoleServer { + common: Arc, + task: Mutex>>, +} + +impl PunchBothEasySymHoleServer { + pub(crate) fn new(common: Arc) -> Self { + Self { + common, + task: Mutex::new(None), + } + } + + // hard sym means public port is random and cannot be predicted + #[tracing::instrument(skip(self), ret, err)] + pub(crate) async fn send_punch_packet_both_easy_sym( + &self, + request: SendPunchPacketBothEasySymRequest, + ) -> Result { + tracing::info!("send_punch_packet_both_easy_sym start"); + let busy_resp = Ok(SendPunchPacketBothEasySymResponse { + is_busy: true, + ..Default::default() + }); + let Ok(mut locked_task) = self.task.try_lock() else { + return busy_resp; + }; + if locked_task.is_some() && !locked_task.as_ref().unwrap().is_finished() { + return busy_resp; + } + + let global_ctx = self.common.get_global_ctx(); + let cur_mapped_addr = global_ctx + .get_stun_info_collector() + .get_udp_port_mapping(0) + .await + .with_context(|| "failed to get udp port mapping")?; + + tracing::info!("send_punch_packet_hard_sym start"); + let socket_count = request.udp_socket_count as usize; + let public_ips = request + .public_ip + .ok_or(anyhow::anyhow!("public_ip is required"))?; + let transaction_id = request.transaction_id; + + let udp_array = + UdpSocketArray::new(socket_count, self.common.get_global_ctx().net_ns.clone()); + udp_array.start().await?; + udp_array.add_intreast_tid(transaction_id); + let peer_mgr = self.common.get_peer_mgr(); + + let punch_packet = + new_hole_punch_packet(transaction_id, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes(); + let mut punched = vec![]; + let common = self.common.clone(); + + let task = tokio::spawn(async move { + let mut listeners = Vec::new(); + let start_time = Instant::now(); + let wait_time_ms = request.wait_time_ms.min(8000); + while start_time.elapsed() < Duration::from_millis(wait_time_ms as u64) { + if let Err(e) = udp_array + .send_with_all( + &punch_packet, + SocketAddr::V4(SocketAddrV4::new( + public_ips.into(), + request.dst_port_num as u16, + )), + ) + .await + { + tracing::error!(?e, "failed to send hole punch packet"); + break; + } + + tokio::time::sleep(Duration::from_millis(100)).await; + + if let Some(s) = udp_array.try_fetch_punched_socket(transaction_id) { + tracing::info!(?s, ?transaction_id, "got punched socket in both easy sym"); + assert!(Arc::strong_count(&s.socket) == 1); + let Some(port) = s.socket.local_addr().ok().map(|addr| addr.port()) else { + tracing::warn!("failed to get local addr from punched socket"); + continue; + }; + let remote_addr = s.remote_addr; + drop(s); + + let listener = + match UdpHolePunchListener::new_ext(peer_mgr.clone(), false, Some(port)) + .await + { + Ok(l) => l, + Err(e) => { + tracing::warn!(?e, "failed to create listener"); + continue; + } + }; + punched.push((listener.get_socket().await, remote_addr)); + listeners.push(listener); + } + + // if any listener is punched, we can break the loop + for l in &listeners { + if l.get_conn_count().await > 0 { + tracing::info!(?l, "got punched listener"); + break; + } + } + + if !punched.is_empty() { + tracing::debug!(?punched, "got punched socket and keep sending punch packet"); + } + + for p in &punched { + let (socket, remote_addr) = p; + let send_remote_ret = socket.send_to(&punch_packet, remote_addr).await; + tracing::debug!( + ?send_remote_ret, + ?socket, + "send hole punch packet to punched remote" + ); + } + } + + for l in listeners { + if l.get_conn_count().await > 0 { + common.add_listener(l).await; + } + } + }); + + *locked_task = Some(task.into()); + return Ok(SendPunchPacketBothEasySymResponse { + is_busy: false, + base_mapped_addr: Some(cur_mapped_addr.into()), + }); + } +} + +#[derive(Debug)] +pub(crate) struct PunchBothEasySymHoleClient { + peer_mgr: Arc, +} + +impl PunchBothEasySymHoleClient { + pub(crate) fn new(peer_mgr: Arc) -> Self { + Self { peer_mgr } + } + + #[tracing::instrument(ret)] + pub(crate) async fn do_hole_punching( + &self, + dst_peer_id: PeerId, + my_nat_info: UdpNatType, + peer_nat_info: UdpNatType, + is_busy: &mut bool, + ) -> Result>, anyhow::Error> { + *is_busy = false; + + let udp_array = UdpSocketArray::new( + UDP_ARRAY_SIZE_FOR_BOTH_EASY_SYM, + self.peer_mgr.get_global_ctx().net_ns.clone(), + ); + udp_array.start().await?; + + let global_ctx = self.peer_mgr.get_global_ctx(); + let cur_mapped_addr = global_ctx + .get_stun_info_collector() + .get_udp_port_mapping(0) + .await + .with_context(|| "failed to get udp port mapping")?; + let my_public_ip = match cur_mapped_addr.ip() { + IpAddr::V4(v4) => v4, + _ => { + anyhow::bail!("ipv6 is not supported"); + } + }; + let me_is_incremental = my_nat_info + .get_inc_of_easy_sym() + .ok_or(anyhow::anyhow!("me_is_incremental is required"))?; + let peer_is_incremental = peer_nat_info + .get_inc_of_easy_sym() + .ok_or(anyhow::anyhow!("peer_is_incremental is required"))?; + + let rpc_stub = self + .peer_mgr + .get_peer_rpc_mgr() + .rpc_client() + .scoped_client::>( + self.peer_mgr.my_peer_id(), + dst_peer_id, + global_ctx.get_network_name(), + ); + + let tid = rand::random(); + udp_array.add_intreast_tid(tid); + + let remote_ret = rpc_stub + .send_punch_packet_both_easy_sym( + BaseController { + timeout_ms: 2000, + ..Default::default() + }, + SendPunchPacketBothEasySymRequest { + transaction_id: tid, + public_ip: Some(my_public_ip.into()), + dst_port_num: if me_is_incremental { + cur_mapped_addr.port().saturating_add(DST_PORT_OFFSET) + } else { + cur_mapped_addr.port().saturating_sub(DST_PORT_OFFSET) + } as u32, + udp_socket_count: UDP_ARRAY_SIZE_FOR_BOTH_EASY_SYM as u32, + wait_time_ms: REMOTE_WAIT_TIME_MS as u32, + }, + ) + .await?; + if remote_ret.is_busy { + *is_busy = true; + anyhow::bail!("remote is busy"); + } + + let mut remote_mapped_addr = remote_ret + .base_mapped_addr + .ok_or(anyhow::anyhow!("remote_mapped_addr is required"))?; + + let now = Instant::now(); + remote_mapped_addr.port = if peer_is_incremental { + remote_mapped_addr + .port + .saturating_add(DST_PORT_OFFSET as u32) + } else { + remote_mapped_addr + .port + .saturating_sub(DST_PORT_OFFSET as u32) + }; + tracing::debug!( + ?remote_mapped_addr, + ?remote_ret, + "start send hole punch packet for both easy sym" + ); + + while now.elapsed().as_millis() < (REMOTE_WAIT_TIME_MS + 1000).into() { + udp_array + .send_with_all( + &new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes(), + remote_mapped_addr.into(), + ) + .await?; + + tokio::time::sleep(Duration::from_millis(100)).await; + + let Some(socket) = udp_array.try_fetch_punched_socket(tid) else { + tracing::trace!( + ?remote_mapped_addr, + ?tid, + "no punched socket found, send some more hole punch packets" + ); + continue; + }; + + tracing::info!( + ?socket, + ?remote_mapped_addr, + ?tid, + "got punched socket in both easy sym" + ); + + for _ in 0..2 { + match try_connect_with_socket(socket.socket.clone(), remote_mapped_addr.into()) + .await + { + Ok(tunnel) => { + return Ok(Some(tunnel)); + } + Err(e) => { + tracing::error!(?e, "failed to connect with socket"); + continue; + } + } + } + udp_array.add_new_socket(socket.socket).await?; + } + + Ok(None) + } +} + +#[cfg(test)] +pub mod tests { + use std::{ + sync::{atomic::AtomicU32, Arc}, + time::Duration, + }; + + use tokio::net::UdpSocket; + + use crate::connector::udp_hole_punch::RUN_TESTING; + use crate::{ + connector::udp_hole_punch::{ + tests::create_mock_peer_manager_with_mock_stun, UdpHolePunchConnector, + }, + peers::tests::{connect_peer_manager, wait_route_appear}, + proto::common::NatType, + tunnel::common::tests::wait_for_condition, + }; + + #[rstest::rstest] + #[tokio::test] + #[serial_test::serial(hole_punch)] + async fn hole_punching_easy_sym(#[values("true", "false")] is_inc: bool) { + RUN_TESTING.store(true, std::sync::atomic::Ordering::Relaxed); + + let p_a = create_mock_peer_manager_with_mock_stun(if is_inc { + NatType::SymmetricEasyInc + } else { + NatType::SymmetricEasyDec + }) + .await; + let p_b = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await; + let p_c = create_mock_peer_manager_with_mock_stun(if !is_inc { + NatType::SymmetricEasyInc + } else { + NatType::SymmetricEasyDec + }) + .await; + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_b.clone(), p_c.clone()).await; + wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap(); + + let mut hole_punching_a = UdpHolePunchConnector::new(p_a.clone()); + let mut hole_punching_c = UdpHolePunchConnector::new(p_c.clone()); + + hole_punching_a.run().await.unwrap(); + hole_punching_c.run().await.unwrap(); + + // 144 + DST_PORT_OFFSET = 164 + let udp1 = Arc::new(UdpSocket::bind("0.0.0.0:40164").await.unwrap()); + // 144 - DST_PORT_OFFSET = 124 + let udp2 = Arc::new(UdpSocket::bind("0.0.0.0:40124").await.unwrap()); + let udps = vec![udp1, udp2]; + + let counter = Arc::new(AtomicU32::new(0)); + + // all these sockets should receive hole punching packet + for udp in udps.iter().map(Arc::clone) { + let counter = counter.clone(); + tokio::spawn(async move { + let mut buf = [0u8; 1024]; + let (len, addr) = udp.recv_from(&mut buf).await.unwrap(); + println!( + "got predictable punch packet, {:?} {:?} {:?}", + len, + addr, + udp.local_addr() + ); + counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + }); + } + + hole_punching_a.client.run_immediately().await; + let udp_len = udps.len(); + wait_for_condition( + || async { counter.load(std::sync::atomic::Ordering::Relaxed) == udp_len as u32 }, + Duration::from_secs(30), + ) + .await; + } +} diff --git a/rust/easytier/src/connector/udp_hole_punch/common.rs b/rust/easytier/src/connector/udp_hole_punch/common.rs new file mode 100644 index 0000000..11cbd06 --- /dev/null +++ b/rust/easytier/src/connector/udp_hole_punch/common.rs @@ -0,0 +1,598 @@ +use std::{ + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + sync::Arc, + time::Duration, +}; + +use crossbeam::atomic::AtomicCell; +use dashmap::{DashMap, DashSet}; +use rand::seq::SliceRandom as _; +use tokio::{net::UdpSocket, sync::Mutex, task::JoinSet}; +use tracing::{instrument, Instrument, Level}; +use zerocopy::FromBytes as _; + +use crate::{ + common::{ + error::Error, global_ctx::ArcGlobalCtx, join_joinset_background, netns::NetNS, + stun::StunInfoCollectorTrait as _, PeerId, + }, + defer, + peers::peer_manager::PeerManager, + proto::common::NatType, + tunnel::{ + packet_def::{UDPTunnelHeader, UdpPacketType, UDP_TUNNEL_HEADER_SIZE}, + udp::{new_hole_punch_packet, UdpTunnelConnector, UdpTunnelListener}, + Tunnel, TunnelConnCounter, TunnelListener as _, + }, +}; + +pub(crate) const HOLE_PUNCH_PACKET_BODY_LEN: u16 = 16; + +fn generate_shuffled_port_vec() -> Vec { + let mut rng = rand::thread_rng(); + let mut port_vec: Vec = (1..=65535).collect(); + port_vec.shuffle(&mut rng); + port_vec +} + +pub(crate) enum UdpPunchClientMethod { + None, + ConeToCone, + SymToCone, + EasySymToEasySym, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum UdpNatType { + Unknown, + Open(NatType), + Cone(NatType), + // bool means if it is incremental + EasySymmetric(NatType, bool), + HardSymmetric(NatType), +} + +impl From for UdpNatType { + fn from(nat_type: NatType) -> Self { + match nat_type { + NatType::Unknown => UdpNatType::Unknown, + NatType::NoPat | NatType::OpenInternet => UdpNatType::Open(nat_type), + NatType::FullCone | NatType::Restricted | NatType::PortRestricted => { + UdpNatType::Cone(nat_type) + } + NatType::Symmetric | NatType::SymUdpFirewall => UdpNatType::HardSymmetric(nat_type), + NatType::SymmetricEasyInc => UdpNatType::EasySymmetric(nat_type, true), + NatType::SymmetricEasyDec => UdpNatType::EasySymmetric(nat_type, false), + } + } +} + +impl Into for UdpNatType { + fn into(self) -> NatType { + match self { + UdpNatType::Unknown => NatType::Unknown, + UdpNatType::Open(nat_type) => nat_type, + UdpNatType::Cone(nat_type) => nat_type, + UdpNatType::EasySymmetric(nat_type, _) => nat_type, + UdpNatType::HardSymmetric(nat_type) => nat_type, + } + } +} + +impl UdpNatType { + pub(crate) fn is_open(&self) -> bool { + matches!(self, UdpNatType::Open(_)) + } + + pub(crate) fn is_unknown(&self) -> bool { + matches!(self, UdpNatType::Unknown) + } + + pub(crate) fn is_sym(&self) -> bool { + self.is_hard_sym() || self.is_easy_sym() + } + + pub(crate) fn is_hard_sym(&self) -> bool { + matches!(self, UdpNatType::HardSymmetric(_)) + } + + pub(crate) fn is_easy_sym(&self) -> bool { + matches!(self, UdpNatType::EasySymmetric(_, _)) + } + + pub(crate) fn is_cone(&self) -> bool { + matches!(self, UdpNatType::Cone(_)) + } + + pub(crate) fn get_inc_of_easy_sym(&self) -> Option { + match self { + UdpNatType::EasySymmetric(_, inc) => Some(*inc), + _ => None, + } + } + + pub(crate) fn get_punch_hole_method(&self, other: Self) -> UdpPunchClientMethod { + if other.is_unknown() { + if self.is_sym() { + return UdpPunchClientMethod::SymToCone; + } else { + return UdpPunchClientMethod::ConeToCone; + } + } + + if self.is_unknown() { + if other.is_sym() { + return UdpPunchClientMethod::None; + } else { + return UdpPunchClientMethod::ConeToCone; + } + } + + if self.is_open() || other.is_open() { + // open nat does not need to punch hole + return UdpPunchClientMethod::None; + } + + if self.is_cone() { + if other.is_sym() { + return UdpPunchClientMethod::None; + } else { + return UdpPunchClientMethod::ConeToCone; + } + } else if self.is_easy_sym() { + if other.is_hard_sym() { + return UdpPunchClientMethod::None; + } else if other.is_easy_sym() { + return UdpPunchClientMethod::EasySymToEasySym; + } else { + return UdpPunchClientMethod::SymToCone; + } + } else if self.is_hard_sym() { + if other.is_sym() { + return UdpPunchClientMethod::None; + } else { + return UdpPunchClientMethod::SymToCone; + } + } + + unreachable!("invalid nat type"); + } + + pub(crate) fn can_punch_hole_as_client( + &self, + other: Self, + my_peer_id: PeerId, + dst_peer_id: PeerId, + ) -> bool { + match self.get_punch_hole_method(other) { + UdpPunchClientMethod::None => false, + UdpPunchClientMethod::ConeToCone | UdpPunchClientMethod::SymToCone => true, + UdpPunchClientMethod::EasySymToEasySym => my_peer_id < dst_peer_id, + } + } +} + +#[derive(Debug)] +pub(crate) struct PunchedUdpSocket { + pub(crate) socket: Arc, + pub(crate) tid: u32, + pub(crate) remote_addr: SocketAddr, +} + +// used for symmetric hole punching, binding to multiple ports to increase the chance of success +pub(crate) struct UdpSocketArray { + sockets: Arc>>, + max_socket_count: usize, + net_ns: NetNS, + tasks: Arc>>, + + intreast_tids: Arc>, + tid_to_socket: Arc>>, +} + +impl UdpSocketArray { + pub fn new(max_socket_count: usize, net_ns: NetNS) -> Self { + let tasks = Arc::new(std::sync::Mutex::new(JoinSet::new())); + join_joinset_background(tasks.clone(), "UdpSocketArray".to_owned()); + + Self { + sockets: Arc::new(DashMap::new()), + max_socket_count, + net_ns, + tasks, + + intreast_tids: Arc::new(DashSet::new()), + tid_to_socket: Arc::new(DashMap::new()), + } + } + + pub fn started(&self) -> bool { + !self.sockets.is_empty() + } + + pub async fn add_new_socket(&self, socket: Arc) -> Result<(), anyhow::Error> { + let socket_map = self.sockets.clone(); + let local_addr = socket.local_addr()?; + let intreast_tids = self.intreast_tids.clone(); + let tid_to_socket = self.tid_to_socket.clone(); + socket_map.insert(local_addr, socket.clone()); + self.tasks.lock().unwrap().spawn( + async move { + defer!(socket_map.remove(&local_addr);); + let mut buf = [0u8; UDP_TUNNEL_HEADER_SIZE + HOLE_PUNCH_PACKET_BODY_LEN as usize]; + tracing::trace!(?local_addr, "udp socket added"); + loop { + let Ok((len, addr)) = socket.recv_from(&mut buf).await else { + break; + }; + + tracing::debug!(?len, ?addr, "got raw packet"); + + if len != UDP_TUNNEL_HEADER_SIZE + HOLE_PUNCH_PACKET_BODY_LEN as usize { + continue; + } + + let Some(p) = UDPTunnelHeader::ref_from_prefix(&buf) else { + continue; + }; + + let tid = p.conn_id.get(); + let valid = p.msg_type == UdpPacketType::HolePunch as u8 + && p.len.get() == HOLE_PUNCH_PACKET_BODY_LEN; + tracing::debug!(?p, ?addr, ?tid, ?valid, ?p, "got udp hole punch packet"); + + if !valid { + continue; + } + + if intreast_tids.contains(&tid) { + tracing::info!(?addr, ?tid, "got hole punching packet with intreast tid"); + tid_to_socket + .entry(tid) + .or_insert_with(Vec::new) + .push(PunchedUdpSocket { + socket: socket.clone(), + tid, + remote_addr: addr, + }); + break; + } + } + tracing::debug!(?local_addr, "udp socket recv loop end"); + } + .instrument(tracing::info_span!("udp array socket recv loop")), + ); + Ok(()) + } + + #[instrument(err)] + pub async fn start(&self) -> Result<(), anyhow::Error> { + tracing::info!("starting udp socket array"); + + while self.sockets.len() < self.max_socket_count { + let socket = { + let _g = self.net_ns.guard(); + Arc::new(UdpSocket::bind("0.0.0.0:0").await?) + }; + + self.add_new_socket(socket).await?; + } + + Ok(()) + } + + #[instrument(err)] + pub async fn send_with_all(&self, data: &[u8], addr: SocketAddr) -> Result<(), anyhow::Error> { + tracing::info!(?addr, "sending hole punching packet"); + + let sockets = self + .sockets + .iter() + .map(|s| s.value().clone()) + .collect::>(); + + for socket in sockets.iter() { + for _ in 0..3 { + socket.send_to(data, addr).await?; + } + } + + Ok(()) + } + + #[instrument(ret(level = Level::DEBUG))] + pub fn try_fetch_punched_socket(&self, tid: u32) -> Option { + tracing::debug!(?tid, "try fetch punched socket"); + self.tid_to_socket.get_mut(&tid)?.value_mut().pop() + } + + pub fn add_intreast_tid(&self, tid: u32) { + self.intreast_tids.insert(tid); + } + + pub fn remove_intreast_tid(&self, tid: u32) { + self.intreast_tids.remove(&tid); + self.tid_to_socket.remove(&tid); + } +} + +impl std::fmt::Debug for UdpSocketArray { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UdpSocketArray") + .field("sockets", &self.sockets.len()) + .field("max_socket_count", &self.max_socket_count) + .field("started", &self.started()) + .field("intreast_tids", &self.intreast_tids.len()) + .field("tid_to_socket", &self.tid_to_socket.len()) + .finish() + } +} + +#[derive(Debug)] +pub(crate) struct UdpHolePunchListener { + socket: Arc, + tasks: JoinSet<()>, + running: Arc>, + mapped_addr: SocketAddr, + conn_counter: Arc>, + + listen_time: std::time::Instant, + last_select_time: AtomicCell, + last_active_time: Arc>, +} + +impl UdpHolePunchListener { + async fn get_avail_port() -> Result { + let socket = UdpSocket::bind("0.0.0.0:0").await?; + Ok(socket.local_addr()?.port()) + } + + #[instrument(err)] + pub async fn new(peer_mgr: Arc) -> Result { + Self::new_ext(peer_mgr, true, None).await + } + + #[instrument(err)] + pub async fn new_ext( + peer_mgr: Arc, + with_mapped_addr: bool, + port: Option, + ) -> Result { + let port = port.unwrap_or(Self::get_avail_port().await?); + let listen_url = format!("udp://0.0.0.0:{}", port); + + let mapped_addr = if with_mapped_addr { + let gctx = peer_mgr.get_global_ctx(); + let stun_info_collect = gctx.get_stun_info_collector(); + stun_info_collect.get_udp_port_mapping(port).await? + } else { + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port)) + }; + + let mut listener = UdpTunnelListener::new(listen_url.parse().unwrap()); + + { + let _g = peer_mgr.get_global_ctx().net_ns.guard(); + listener.listen().await?; + } + let socket = listener.get_socket().unwrap(); + + let running = Arc::new(AtomicCell::new(true)); + let running_clone = running.clone(); + + let conn_counter = listener.get_conn_counter(); + let mut tasks = JoinSet::new(); + + tasks.spawn(async move { + while let Ok(conn) = listener.accept().await { + tracing::warn!(?conn, "udp hole punching listener got peer connection"); + let peer_mgr = peer_mgr.clone(); + tokio::spawn(async move { + if let Err(e) = peer_mgr.add_tunnel_as_server(conn).await { + tracing::error!( + ?e, + "failed to add tunnel as server in hole punch listener" + ); + } + }); + } + + running_clone.store(false); + }); + + let last_active_time = Arc::new(AtomicCell::new(std::time::Instant::now())); + let conn_counter_clone = conn_counter.clone(); + let last_active_time_clone = last_active_time.clone(); + tasks.spawn(async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + if conn_counter_clone.get().unwrap_or(0) != 0 { + last_active_time_clone.store(std::time::Instant::now()); + } + } + }); + + tracing::warn!(?mapped_addr, ?socket, "udp hole punching listener started"); + + Ok(Self { + tasks, + socket, + running, + mapped_addr, + conn_counter, + + listen_time: std::time::Instant::now(), + last_select_time: AtomicCell::new(std::time::Instant::now()), + last_active_time, + }) + } + + pub async fn get_socket(&self) -> Arc { + self.last_select_time.store(std::time::Instant::now()); + self.socket.clone() + } + + pub async fn get_conn_count(&self) -> usize { + self.conn_counter.get().unwrap_or(0) as usize + } +} + +pub(crate) struct PunchHoleServerCommon { + peer_mgr: Arc, + + listeners: Arc>>, + tasks: Arc>>, +} + +impl PunchHoleServerCommon { + pub(crate) fn new(peer_mgr: Arc) -> Self { + let tasks = Arc::new(std::sync::Mutex::new(JoinSet::new())); + join_joinset_background(tasks.clone(), "PunchHoleServerCommon".to_owned()); + + let listeners = Arc::new(Mutex::new(Vec::::new())); + + let l = listeners.clone(); + tasks.lock().unwrap().spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(5)).await; + { + // remove listener that is not active for 40 seconds but keep listeners that are selected less than 30 seconds + l.lock().await.retain(|listener| { + listener.last_active_time.load().elapsed().as_secs() < 40 + || listener.last_select_time.load().elapsed().as_secs() < 30 + }); + } + } + }); + + Self { + peer_mgr, + + listeners, + tasks, + } + } + + pub(crate) async fn add_listener(&self, listener: UdpHolePunchListener) { + self.listeners.lock().await.push(listener); + } + + pub(crate) async fn find_listener(&self, addr: &SocketAddr) -> Option> { + let all_listener_sockets = self.listeners.lock().await; + + let listener = all_listener_sockets + .iter() + .find(|listener| listener.mapped_addr == *addr && listener.running.load())?; + + Some(listener.get_socket().await) + } + + pub(crate) async fn my_udp_nat_type(&self) -> i32 { + self.peer_mgr + .get_global_ctx() + .get_stun_info_collector() + .get_stun_info() + .udp_nat_type + } + + pub(crate) async fn select_listener( + &self, + use_new_listener: bool, + ) -> Option<(Arc, SocketAddr)> { + let all_listener_sockets = &self.listeners; + + let mut use_last = false; + if all_listener_sockets.lock().await.len() < 16 || use_new_listener { + tracing::warn!("creating new udp hole punching listener"); + all_listener_sockets.lock().await.push( + UdpHolePunchListener::new(self.peer_mgr.clone()) + .await + .ok()?, + ); + use_last = true; + } + + let mut locked = all_listener_sockets.lock().await; + + let listener = if use_last { + locked.last_mut()? + } else { + // use the listener that is active most recently + locked + .iter_mut() + .max_by_key(|listener| listener.last_active_time.load())? + }; + + if listener.mapped_addr.ip().is_unspecified() { + tracing::info!("listener mapped addr is unspecified, trying to get mapped addr"); + listener.mapped_addr = self + .get_global_ctx() + .get_stun_info_collector() + .get_udp_port_mapping(listener.mapped_addr.port()) + .await + .ok()?; + } + + Some((listener.get_socket().await, listener.mapped_addr)) + } + + pub(crate) fn get_joinset(&self) -> Arc>> { + self.tasks.clone() + } + + pub(crate) fn get_global_ctx(&self) -> ArcGlobalCtx { + self.peer_mgr.get_global_ctx() + } + + pub(crate) fn get_peer_mgr(&self) -> Arc { + self.peer_mgr.clone() + } +} + +#[tracing::instrument(err, ret(level=Level::DEBUG), skip(ports))] +pub(crate) async fn send_symmetric_hole_punch_packet( + ports: &Vec, + udp: Arc, + transaction_id: u32, + public_ips: &Vec, + port_start_idx: usize, + max_packets: usize, +) -> Result { + tracing::debug!("sending hard symmetric hole punching packet"); + let mut sent_packets = 0; + let mut cur_port_idx = port_start_idx; + while sent_packets < max_packets { + let port = ports[cur_port_idx % ports.len()]; + for pub_ip in public_ips { + let addr = SocketAddr::V4(SocketAddrV4::new(*pub_ip, port)); + for _ in 0..3 { + let packet = new_hole_punch_packet(transaction_id, HOLE_PUNCH_PACKET_BODY_LEN); + udp.send_to(&packet.into_bytes(), addr).await?; + } + sent_packets += 1; + } + cur_port_idx = cur_port_idx.wrapping_add(1); + tokio::time::sleep(Duration::from_millis(1)).await; + } + Ok(cur_port_idx % ports.len()) +} + +pub(crate) async fn try_connect_with_socket( + socket: Arc, + remote_mapped_addr: SocketAddr, +) -> Result, Error> { + let connector = UdpTunnelConnector::new( + format!( + "udp://{}:{}", + remote_mapped_addr.ip(), + remote_mapped_addr.port() + ) + .to_string() + .parse() + .unwrap(), + ); + connector + .try_connect_with_socket(socket, remote_mapped_addr) + .await + .map_err(|e| Error::from(e)) +} diff --git a/rust/easytier/src/connector/udp_hole_punch/cone.rs b/rust/easytier/src/connector/udp_hole_punch/cone.rs new file mode 100644 index 0000000..7e11e80 --- /dev/null +++ b/rust/easytier/src/connector/udp_hole_punch/cone.rs @@ -0,0 +1,264 @@ +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; + +use anyhow::Context; +use tokio::net::UdpSocket; + +use crate::{ + common::{scoped_task::ScopedTask, stun::StunInfoCollectorTrait, PeerId}, + connector::udp_hole_punch::common::{ + try_connect_with_socket, UdpSocketArray, HOLE_PUNCH_PACKET_BODY_LEN, + }, + peers::peer_manager::PeerManager, + proto::{ + common::Void, + peer_rpc::{ + SelectPunchListenerRequest, SendPunchPacketConeRequest, UdpHolePunchRpcClientFactory, + }, + rpc_types::{self, controller::BaseController}, + }, + tunnel::{udp::new_hole_punch_packet, Tunnel}, +}; + +use super::common::PunchHoleServerCommon; + +pub(crate) struct PunchConeHoleServer { + common: Arc, +} + +impl PunchConeHoleServer { + pub(crate) fn new(common: Arc) -> Self { + Self { common } + } + + #[tracing::instrument(skip(self), ret, err)] + pub(crate) async fn send_punch_packet_cone( + &self, + _: BaseController, + request: SendPunchPacketConeRequest, + ) -> Result { + let listener_addr = request.listener_mapped_addr.ok_or(anyhow::anyhow!( + "send_punch_packet_for_cone request missing listener_mapped_addr" + ))?; + let listener_addr = std::net::SocketAddr::from(listener_addr); + let listener = self + .common + .find_listener(&listener_addr) + .await + .ok_or(anyhow::anyhow!( + "send_punch_packet_for_cone failed to find listener" + ))?; + + let dest_addr = request.dest_addr.ok_or(anyhow::anyhow!( + "send_punch_packet_for_cone request missing dest_addr" + ))?; + let dest_addr = std::net::SocketAddr::from(dest_addr); + let dest_ip = dest_addr.ip(); + if dest_ip.is_unspecified() || dest_ip.is_multicast() { + return Err(anyhow::anyhow!( + "send_punch_packet_for_cone dest_ip is malformed, {:?}", + request + ) + .into()); + } + + for _ in 0..request.packet_batch_count { + tracing::info!(?request, "sending hole punching packet"); + + for _ in 0..request.packet_count_per_batch { + let udp_packet = + new_hole_punch_packet(request.transaction_id, HOLE_PUNCH_PACKET_BODY_LEN); + if let Err(e) = listener.send_to(&udp_packet.into_bytes(), &dest_addr).await { + tracing::error!(?e, "failed to send hole punch packet to dest addr"); + } + } + tokio::time::sleep(Duration::from_millis(request.packet_interval_ms as u64)).await; + } + + Ok(Void::default()) + } +} + +pub(crate) struct PunchConeHoleClient { + peer_mgr: Arc, +} + +impl PunchConeHoleClient { + pub(crate) fn new(peer_mgr: Arc) -> Self { + Self { peer_mgr } + } + + #[tracing::instrument(skip(self))] + pub(crate) async fn do_hole_punching( + &self, + dst_peer_id: PeerId, + ) -> Result>, anyhow::Error> { + tracing::info!(?dst_peer_id, "start hole punching"); + let tid = rand::random(); + + let global_ctx = self.peer_mgr.get_global_ctx(); + let udp_array = UdpSocketArray::new(1, global_ctx.net_ns.clone()); + let local_socket = { + let _g = self.peer_mgr.get_global_ctx().net_ns.guard(); + Arc::new(UdpSocket::bind("0.0.0.0:0").await?) + }; + + let local_addr = local_socket + .local_addr() + .with_context(|| anyhow::anyhow!("failed to get local port from udp array"))?; + let local_port = local_addr.port(); + + drop(local_socket); + let local_mapped_addr = global_ctx + .get_stun_info_collector() + .get_udp_port_mapping(local_port) + .await + .with_context(|| "failed to get udp port mapping")?; + + let local_socket = { + let _g = self.peer_mgr.get_global_ctx().net_ns.guard(); + Arc::new(UdpSocket::bind(local_addr).await?) + }; + + // client -> server: tell server the mapped port, server will return the mapped address of listening port. + let rpc_stub = self + .peer_mgr + .get_peer_rpc_mgr() + .rpc_client() + .scoped_client::>( + self.peer_mgr.my_peer_id(), + dst_peer_id, + global_ctx.get_network_name(), + ); + + let resp = rpc_stub + .select_punch_listener( + BaseController::default(), + SelectPunchListenerRequest { force_new: false }, + ) + .await + .with_context(|| "failed to select punch listener")?; + let remote_mapped_addr = resp.listener_mapped_addr.ok_or(anyhow::anyhow!( + "select_punch_listener response missing listener_mapped_addr" + ))?; + + tracing::debug!( + ?local_mapped_addr, + ?remote_mapped_addr, + "hole punch got remote listener" + ); + + udp_array.add_new_socket(local_socket).await?; + udp_array.add_intreast_tid(tid); + let send_from_local = || async { + udp_array + .send_with_all( + &new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes(), + remote_mapped_addr.clone().into(), + ) + .await + .with_context(|| "failed to send hole punch packet from local") + }; + + send_from_local().await?; + + let scoped_punch_task: ScopedTask<()> = tokio::spawn(async move { + if let Err(e) = rpc_stub + .send_punch_packet_cone( + BaseController { + timeout_ms: 4000, + ..Default::default() + }, + SendPunchPacketConeRequest { + listener_mapped_addr: Some(remote_mapped_addr.into()), + dest_addr: Some(local_mapped_addr.into()), + transaction_id: tid, + packet_count_per_batch: 2, + packet_batch_count: 5, + packet_interval_ms: 400, + }, + ) + .await + { + tracing::error!(?e, "failed to call remote send punch packet"); + } + }) + .into(); + + // server: will send some punching resps, total 10 packets. + // client: use the socket to create UdpTunnel with UdpTunnelConnector + // NOTICE: UdpTunnelConnector will ignore the punching resp packet sent by remote. + let mut finish_time: Option = None; + while finish_time.is_none() || finish_time.as_ref().unwrap().elapsed().as_millis() < 1000 { + tokio::time::sleep(Duration::from_millis(200)).await; + + if finish_time.is_none() && (*scoped_punch_task).is_finished() { + finish_time = Some(Instant::now()); + } + + let Some(socket) = udp_array.try_fetch_punched_socket(tid) else { + tracing::debug!("no punched socket found, send some more hole punch packets"); + send_from_local().await?; + continue; + }; + + tracing::debug!(?socket, ?tid, "punched socket found, try connect with it"); + + for _ in 0..2 { + match try_connect_with_socket(socket.socket.clone(), remote_mapped_addr.into()) + .await + { + Ok(tunnel) => { + tracing::info!(?tunnel, "hole punched"); + return Ok(Some(tunnel)); + } + Err(e) => { + tracing::error!(?e, "failed to connect with socket"); + } + } + } + } + + return Ok(None); + } +} + +#[cfg(test)] +pub mod tests { + + use crate::{ + connector::udp_hole_punch::{ + tests::create_mock_peer_manager_with_mock_stun, UdpHolePunchConnector, + }, + peers::tests::{connect_peer_manager, wait_route_appear, wait_route_appear_with_cost}, + proto::common::NatType, + }; + + #[tokio::test] + async fn hole_punching_cone() { + let p_a = create_mock_peer_manager_with_mock_stun(NatType::Restricted).await; + let p_b = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await; + let p_c = create_mock_peer_manager_with_mock_stun(NatType::Restricted).await; + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_b.clone(), p_c.clone()).await; + + wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap(); + + println!("{:?}", p_a.list_routes().await); + + let mut hole_punching_a = UdpHolePunchConnector::new(p_a.clone()); + let mut hole_punching_c = UdpHolePunchConnector::new(p_c.clone()); + + hole_punching_a.run_as_client().await.unwrap(); + hole_punching_c.run_as_server().await.unwrap(); + + hole_punching_a.client.run_immediately().await; + + wait_route_appear_with_cost(p_a.clone(), p_c.my_peer_id(), Some(1)) + .await + .unwrap(); + println!("{:?}", p_a.list_routes().await); + } +} diff --git a/rust/easytier/src/connector/udp_hole_punch/mod.rs b/rust/easytier/src/connector/udp_hole_punch/mod.rs new file mode 100644 index 0000000..aabb0f6 --- /dev/null +++ b/rust/easytier/src/connector/udp_hole_punch/mod.rs @@ -0,0 +1,559 @@ +use std::sync::{atomic::AtomicBool, Arc}; + +use anyhow::{Context, Error}; +use both_easy_sym::{PunchBothEasySymHoleClient, PunchBothEasySymHoleServer}; +use common::{PunchHoleServerCommon, UdpNatType, UdpPunchClientMethod}; +use cone::{PunchConeHoleClient, PunchConeHoleServer}; +use dashmap::DashMap; +use once_cell::sync::Lazy; +use sym_to_cone::{PunchSymToConeHoleClient, PunchSymToConeHoleServer}; +use tokio::{sync::Mutex, task::JoinHandle}; + +use crate::{ + common::{stun::StunInfoCollectorTrait, PeerId}, + connector::direct::PeerManagerForDirectConnector, + peers::{ + peer_manager::PeerManager, + peer_task::{PeerTaskLauncher, PeerTaskManager}, + }, + proto::{ + common::{NatType, Void}, + peer_rpc::{ + SelectPunchListenerRequest, SelectPunchListenerResponse, + SendPunchPacketBothEasySymRequest, SendPunchPacketBothEasySymResponse, + SendPunchPacketConeRequest, SendPunchPacketEasySymRequest, + SendPunchPacketHardSymRequest, SendPunchPacketHardSymResponse, UdpHolePunchRpc, + UdpHolePunchRpcServer, + }, + rpc_types::{self, controller::BaseController}, + }, + tunnel::Tunnel, +}; + +pub(crate) mod both_easy_sym; +pub(crate) mod common; +pub(crate) mod cone; +pub(crate) mod sym_to_cone; + +// sym punch should be serialized +static SYM_PUNCH_LOCK: Lazy>>> = Lazy::new(|| DashMap::new()); +static RUN_TESTING: Lazy = Lazy::new(|| AtomicBool::new(false)); + +fn get_sym_punch_lock(peer_id: PeerId) -> Arc> { + SYM_PUNCH_LOCK + .entry(peer_id) + .or_insert_with(|| Arc::new(Mutex::new(()))) + .value() + .clone() +} + +struct UdpHolePunchServer { + common: Arc, + cone_server: PunchConeHoleServer, + sym_to_cone_server: PunchSymToConeHoleServer, + both_easy_sym_server: PunchBothEasySymHoleServer, +} + +impl UdpHolePunchServer { + pub fn new(peer_mgr: Arc) -> Arc { + let common = Arc::new(PunchHoleServerCommon::new(peer_mgr.clone())); + let cone_server = PunchConeHoleServer::new(common.clone()); + let sym_to_cone_server = PunchSymToConeHoleServer::new(common.clone()); + let both_easy_sym_server = PunchBothEasySymHoleServer::new(common.clone()); + + Arc::new(Self { + common, + cone_server, + sym_to_cone_server, + both_easy_sym_server, + }) + } +} + +#[async_trait::async_trait] +impl UdpHolePunchRpc for UdpHolePunchServer { + type Controller = BaseController; + + async fn select_punch_listener( + &self, + _ctrl: Self::Controller, + input: SelectPunchListenerRequest, + ) -> rpc_types::error::Result { + let (_, addr) = self + .common + .select_listener(input.force_new) + .await + .ok_or(anyhow::anyhow!("no listener available"))?; + + Ok(SelectPunchListenerResponse { + listener_mapped_addr: Some(addr.into()), + }) + } + + /// send packet to one remote_addr, used by nat1-3 to nat1-3 + async fn send_punch_packet_cone( + &self, + ctrl: Self::Controller, + input: SendPunchPacketConeRequest, + ) -> rpc_types::error::Result { + self.cone_server.send_punch_packet_cone(ctrl, input).await + } + + /// send packet to multiple remote_addr (birthday attack), used by nat4 to nat1-3 + async fn send_punch_packet_hard_sym( + &self, + _ctrl: Self::Controller, + input: SendPunchPacketHardSymRequest, + ) -> rpc_types::error::Result { + let _locked = get_sym_punch_lock(self.common.get_peer_mgr().my_peer_id()) + .try_lock_owned() + .with_context(|| "sym punch lock is busy")?; + self.sym_to_cone_server + .send_punch_packet_hard_sym(input) + .await + } + + async fn send_punch_packet_easy_sym( + &self, + _ctrl: Self::Controller, + input: SendPunchPacketEasySymRequest, + ) -> rpc_types::error::Result { + let _locked = get_sym_punch_lock(self.common.get_peer_mgr().my_peer_id()) + .try_lock_owned() + .with_context(|| "sym punch lock is busy")?; + self.sym_to_cone_server + .send_punch_packet_easy_sym(input) + .await + .map(|_| Void {}) + } + + /// nat4 to nat4 (both predictably) + async fn send_punch_packet_both_easy_sym( + &self, + _ctrl: Self::Controller, + input: SendPunchPacketBothEasySymRequest, + ) -> rpc_types::error::Result { + let _locked = get_sym_punch_lock(self.common.get_peer_mgr().my_peer_id()) + .try_lock_owned() + .with_context(|| "sym punch lock is busy")?; + self.both_easy_sym_server + .send_punch_packet_both_easy_sym(input) + .await + } +} + +#[derive(Debug)] +struct BackOff { + backoffs_ms: Vec, + current_idx: usize, +} + +impl BackOff { + pub fn new(backoffs_ms: Vec) -> Self { + Self { + backoffs_ms, + current_idx: 0, + } + } + + pub fn next_backoff(&mut self) -> u64 { + let backoff = self.backoffs_ms[self.current_idx]; + self.current_idx = (self.current_idx + 1).min(self.backoffs_ms.len() - 1); + backoff + } + + pub fn rollback(&mut self) { + self.current_idx = self.current_idx.saturating_sub(1); + } + + pub async fn sleep_for_next_backoff(&mut self) { + let backoff = self.next_backoff(); + if backoff > 0 { + tokio::time::sleep(tokio::time::Duration::from_millis(backoff)).await; + } + } +} + +struct UdpHoePunchConnectorData { + cone_client: PunchConeHoleClient, + sym_to_cone_client: PunchSymToConeHoleClient, + both_easy_sym_client: PunchBothEasySymHoleClient, + peer_mgr: Arc, +} + +impl UdpHoePunchConnectorData { + pub fn new(peer_mgr: Arc) -> Arc { + let cone_client = PunchConeHoleClient::new(peer_mgr.clone()); + let sym_to_cone_client = PunchSymToConeHoleClient::new(peer_mgr.clone()); + let both_easy_sym_client = PunchBothEasySymHoleClient::new(peer_mgr.clone()); + + Arc::new(Self { + cone_client, + sym_to_cone_client, + both_easy_sym_client, + peer_mgr, + }) + } + + #[tracing::instrument(skip(self))] + async fn handle_punch_result( + self: &Self, + ret: Result>, Error>, + backoff: Option<&mut BackOff>, + round: Option<&mut u32>, + ) -> bool { + let op = |rollback: bool| { + if rollback { + if let Some(backoff) = backoff { + backoff.rollback(); + } + if let Some(round) = round { + *round = round.saturating_sub(1); + } + } else { + if let Some(round) = round { + *round += 1; + } + } + }; + + match ret { + Ok(Some(tunnel)) => { + tracing::info!(?tunnel, "hole punching get tunnel success"); + + if let Err(e) = self.peer_mgr.add_client_tunnel(tunnel).await { + tracing::warn!(?e, "add client tunnel failed"); + op(true); + false + } else { + true + } + } + Ok(None) => { + tracing::info!("hole punching failed, no punch tunnel"); + op(false); + false + } + Err(e) => { + tracing::info!(?e, "hole punching failed"); + op(true); + false + } + } + } + + #[tracing::instrument(skip(self))] + async fn cone_to_cone(self: Arc, task_info: PunchTaskInfo) -> Result<(), Error> { + let mut backoff = BackOff::new(vec![0, 1000, 2000, 4000, 4000, 8000, 8000, 16000]); + + loop { + backoff.sleep_for_next_backoff().await; + + let ret = self + .cone_client + .do_hole_punching(task_info.dst_peer_id) + .await; + + if self + .handle_punch_result(ret, Some(&mut backoff), None) + .await + { + break; + } + } + + Ok(()) + } + + #[tracing::instrument(skip(self))] + async fn sym_to_cone(self: Arc, task_info: PunchTaskInfo) -> Result<(), Error> { + let mut backoff = BackOff::new(vec![0, 1000, 2000, 4000, 4000, 8000, 8000, 16000, 64000]); + let mut round = 0; + let mut port_idx = rand::random(); + + loop { + backoff.sleep_for_next_backoff().await; + + // always try cone first + if !RUN_TESTING.load(std::sync::atomic::Ordering::Relaxed) { + let ret = self + .cone_client + .do_hole_punching(task_info.dst_peer_id) + .await; + if self.handle_punch_result(ret, None, None).await { + break; + } + } + + let ret = { + let _lock = get_sym_punch_lock(self.peer_mgr.my_peer_id()) + .lock_owned() + .await; + self.sym_to_cone_client + .do_hole_punching( + task_info.dst_peer_id, + round, + &mut port_idx, + task_info.my_nat_type, + ) + .await + }; + + if self + .handle_punch_result(ret, Some(&mut backoff), Some(&mut round)) + .await + { + break; + } + } + + Ok(()) + } + + #[tracing::instrument(skip(self))] + async fn both_easy_sym(self: Arc, task_info: PunchTaskInfo) -> Result<(), Error> { + let mut backoff = BackOff::new(vec![0, 1000, 2000, 4000, 4000, 8000, 8000, 16000, 64000]); + + loop { + backoff.sleep_for_next_backoff().await; + + // always try cone first + if !RUN_TESTING.load(std::sync::atomic::Ordering::Relaxed) { + let ret = self + .cone_client + .do_hole_punching(task_info.dst_peer_id) + .await; + if self.handle_punch_result(ret, None, None).await { + break; + } + } + + let mut is_busy = false; + + let ret = { + let _lock = get_sym_punch_lock(self.peer_mgr.my_peer_id()) + .lock_owned() + .await; + self.both_easy_sym_client + .do_hole_punching( + task_info.dst_peer_id, + task_info.my_nat_type, + task_info.dst_nat_type, + &mut is_busy, + ) + .await + }; + + if is_busy { + backoff.rollback(); + } else if self + .handle_punch_result(ret, Some(&mut backoff), None) + .await + { + break; + } + } + + Ok(()) + } +} + +#[derive(Clone)] +struct UdpHolePunchPeerTaskLauncher {} + +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +struct PunchTaskInfo { + dst_peer_id: PeerId, + dst_nat_type: UdpNatType, + my_nat_type: UdpNatType, +} + +#[async_trait::async_trait] +impl PeerTaskLauncher for UdpHolePunchPeerTaskLauncher { + type Data = Arc; + type CollectPeerItem = PunchTaskInfo; + type TaskRet = (); + + fn new_data(&self, peer_mgr: Arc) -> Self::Data { + UdpHoePunchConnectorData::new(peer_mgr) + } + + async fn collect_peers_need_task(&self, data: &Self::Data) -> Vec { + let my_nat_type = data + .peer_mgr + .get_global_ctx() + .get_stun_info_collector() + .get_stun_info() + .udp_nat_type; + let my_nat_type: UdpNatType = NatType::try_from(my_nat_type) + .unwrap_or(NatType::Unknown) + .into(); + if !my_nat_type.is_sym() { + data.sym_to_cone_client.clear_udp_array().await; + } + + let mut peers_to_connect: Vec = Vec::new(); + // do not do anything if: + // 1. our nat type is OpenInternet or NoPat, which means we can wait other peers to connect us + // notice that if we are unknown, we treat ourselves as cone + if my_nat_type.is_open() { + return peers_to_connect; + } + + let my_peer_id = data.peer_mgr.my_peer_id(); + + // collect peer list from peer manager and do some filter: + // 1. peers without direct conns; + // 2. peers is full cone (any restricted type); + for route in data.peer_mgr.list_routes().await.iter() { + if route + .feature_flag + .map(|x| x.is_public_server) + .unwrap_or(false) + { + continue; + } + + let peer_nat_type = route + .stun_info + .as_ref() + .map(|x| x.udp_nat_type) + .unwrap_or(0); + let Ok(peer_nat_type) = NatType::try_from(peer_nat_type) else { + continue; + }; + let peer_nat_type = peer_nat_type.into(); + + let peer_id: PeerId = route.peer_id; + let conns = data.peer_mgr.list_peer_conns(peer_id).await; + if conns.is_some() && conns.unwrap().len() > 0 { + continue; + } + + if !my_nat_type.can_punch_hole_as_client(peer_nat_type, my_peer_id, peer_id) { + continue; + } + + tracing::info!( + ?peer_id, + ?peer_nat_type, + ?my_nat_type, + "found peer to do hole punching" + ); + + peers_to_connect.push(PunchTaskInfo { + dst_peer_id: peer_id, + dst_nat_type: peer_nat_type, + my_nat_type, + }); + } + + peers_to_connect + } + + async fn launch_task( + &self, + data: &Self::Data, + item: Self::CollectPeerItem, + ) -> JoinHandle> { + let data = data.clone(); + let punch_method = item.my_nat_type.get_punch_hole_method(item.dst_nat_type); + match punch_method { + UdpPunchClientMethod::ConeToCone => tokio::spawn(data.cone_to_cone(item)), + UdpPunchClientMethod::SymToCone => tokio::spawn(data.sym_to_cone(item)), + UdpPunchClientMethod::EasySymToEasySym => tokio::spawn(data.both_easy_sym(item)), + _ => unreachable!(), + } + } + + async fn all_task_done(&self, data: &Self::Data) { + data.sym_to_cone_client.clear_udp_array().await; + } + + fn loop_interval_ms(&self) -> u64 { + 5000 + } +} + +pub struct UdpHolePunchConnector { + server: Arc, + client: PeerTaskManager, + peer_mgr: Arc, +} + +// Currently support: +// Symmetric -> Full Cone +// Any Type of Full Cone -> Any Type of Full Cone + +// if same level of full cone, node with smaller peer_id will be the initiator +// if different level of full cone, node with more strict level will be the initiator + +impl UdpHolePunchConnector { + pub fn new(peer_mgr: Arc) -> Self { + Self { + server: UdpHolePunchServer::new(peer_mgr.clone()), + client: PeerTaskManager::new(UdpHolePunchPeerTaskLauncher {}, peer_mgr.clone()), + peer_mgr, + } + } + + pub async fn run_as_client(&mut self) -> Result<(), Error> { + self.client.start(); + Ok(()) + } + + pub async fn run_as_server(&mut self) -> Result<(), Error> { + self.peer_mgr + .get_peer_rpc_mgr() + .rpc_server() + .registry() + .register( + UdpHolePunchRpcServer::new(self.server.clone()), + &self.peer_mgr.get_global_ctx().get_network_name(), + ); + + Ok(()) + } + + pub async fn run(&mut self) -> Result<(), Error> { + let global_ctx = self.peer_mgr.get_global_ctx(); + + if global_ctx.get_flags().disable_p2p { + return Ok(()); + } + if global_ctx.get_flags().disable_udp_hole_punching { + return Ok(()); + } + + self.run_as_client().await?; + self.run_as_server().await?; + + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + + use std::sync::Arc; + + use crate::common::stun::MockStunInfoCollector; + use crate::proto::common::NatType; + + use crate::peers::{peer_manager::PeerManager, tests::create_mock_peer_manager}; + + pub fn replace_stun_info_collector(peer_mgr: Arc, udp_nat_type: NatType) { + let collector = Box::new(MockStunInfoCollector { udp_nat_type }); + peer_mgr + .get_global_ctx() + .replace_stun_info_collector(collector); + } + + pub async fn create_mock_peer_manager_with_mock_stun( + udp_nat_type: NatType, + ) -> Arc { + let p_a = create_mock_peer_manager().await; + replace_stun_info_collector(p_a.clone(), udp_nat_type); + p_a + } +} diff --git a/rust/easytier/src/connector/udp_hole_punch/sym_to_cone.rs b/rust/easytier/src/connector/udp_hole_punch/sym_to_cone.rs new file mode 100644 index 0000000..48c7321 --- /dev/null +++ b/rust/easytier/src/connector/udp_hole_punch/sym_to_cone.rs @@ -0,0 +1,689 @@ +use std::{ + net::Ipv4Addr, + ops::{Div, Mul}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::{Duration, Instant}, +}; + +use anyhow::Context; +use rand::{seq::SliceRandom, Rng}; +use tokio::{net::UdpSocket, sync::RwLock}; +use tracing::Level; + +use crate::{ + common::{scoped_task::ScopedTask, stun::StunInfoCollectorTrait, PeerId}, + connector::udp_hole_punch::common::{ + send_symmetric_hole_punch_packet, try_connect_with_socket, HOLE_PUNCH_PACKET_BODY_LEN, + }, + defer, + peers::peer_manager::PeerManager, + proto::{ + peer_rpc::{ + SelectPunchListenerRequest, SendPunchPacketEasySymRequest, + SendPunchPacketHardSymRequest, SendPunchPacketHardSymResponse, UdpHolePunchRpc, + UdpHolePunchRpcClientFactory, + }, + rpc_types::{self, controller::BaseController}, + }, + tunnel::{udp::new_hole_punch_packet, Tunnel}, +}; + +use super::common::{PunchHoleServerCommon, UdpNatType, UdpSocketArray}; + +const UDP_ARRAY_SIZE_FOR_HARD_SYM: usize = 84; + +pub(crate) struct PunchSymToConeHoleServer { + common: Arc, + + shuffled_port_vec: Arc>, +} + +impl PunchSymToConeHoleServer { + pub(crate) fn new(common: Arc) -> Self { + let mut shuffled_port_vec: Vec = (1..=65535).collect(); + shuffled_port_vec.shuffle(&mut rand::thread_rng()); + + Self { + common, + shuffled_port_vec: Arc::new(shuffled_port_vec), + } + } + + // hard sym means public port is random and cannot be predicted + #[tracing::instrument(skip(self), ret)] + pub(crate) async fn send_punch_packet_easy_sym( + &self, + request: SendPunchPacketEasySymRequest, + ) -> Result<(), rpc_types::error::Error> { + tracing::info!("send_punch_packet_easy_sym start"); + + let listener_addr = request.listener_mapped_addr.ok_or(anyhow::anyhow!( + "send_punch_packet_easy_sym request missing listener_addr" + ))?; + let listener_addr = std::net::SocketAddr::from(listener_addr); + let listener = self + .common + .find_listener(&listener_addr) + .await + .ok_or(anyhow::anyhow!( + "send_punch_packet_easy_sym failed to find listener" + ))?; + + let public_ips = request + .public_ips + .into_iter() + .map(|ip| std::net::Ipv4Addr::from(ip)) + .collect::>(); + if public_ips.len() == 0 { + tracing::warn!("send_punch_packet_easy_sym got zero len public ip"); + return Err( + anyhow::anyhow!("send_punch_packet_easy_sym got zero len public ip").into(), + ); + } + + let transaction_id = request.transaction_id; + let base_port_num = request.base_port_num; + let max_port_num = request.max_port_num.max(1); + let is_incremental = request.is_incremental; + + let port_start = if is_incremental { + base_port_num.saturating_add(1) + } else { + base_port_num.saturating_sub(max_port_num) + }; + + let port_end = if is_incremental { + base_port_num.saturating_add(max_port_num) + } else { + base_port_num.saturating_sub(1) + }; + + if port_end <= port_start { + return Err(anyhow::anyhow!("send_punch_packet_easy_sym invalid port range").into()); + } + + let ports = (port_start..=port_end) + .map(|x| x as u16) + .collect::>(); + tracing::debug!( + ?ports, + ?public_ips, + "send_punch_packet_easy_sym send to ports" + ); + + for _ in 0..2 { + send_symmetric_hole_punch_packet( + &ports, + listener.clone(), + transaction_id, + &public_ips, + 0, + ports.len(), + ) + .await + .with_context(|| "failed to send symmetric hole punch packet")?; + } + + Ok(()) + } + + // hard sym means public port is random and cannot be predicted + #[tracing::instrument(skip(self))] + pub(crate) async fn send_punch_packet_hard_sym( + &self, + request: SendPunchPacketHardSymRequest, + ) -> Result { + tracing::info!("try_punch_symmetric start"); + + let listener_addr = request.listener_mapped_addr.ok_or(anyhow::anyhow!( + "try_punch_symmetric request missing listener_addr" + ))?; + let listener_addr = std::net::SocketAddr::from(listener_addr); + let listener = self + .common + .find_listener(&listener_addr) + .await + .ok_or(anyhow::anyhow!( + "send_punch_packet_for_cone failed to find listener" + ))?; + + let public_ips = request + .public_ips + .into_iter() + .map(|ip| std::net::Ipv4Addr::from(ip)) + .collect::>(); + if public_ips.len() == 0 { + tracing::warn!("try_punch_symmetric got zero len public ip"); + return Err(anyhow::anyhow!("try_punch_symmetric got zero len public ip").into()); + } + + let transaction_id = request.transaction_id; + let last_port_index = request.port_index as usize; + + let round = std::cmp::max(request.round, 1); + + // send max k1 packets if we are predicting the dst port + let max_k1: u32 = 180; + // send max k2 packets if we are sending to random port + let mut max_k2: u32 = rand::thread_rng().gen_range(600..800); + if round > 2 { + max_k2 = max_k2.mul(2).div(round).max(max_k1); + } + + let mut next_port_index = 0; + for _ in 0..2 { + next_port_index = send_symmetric_hole_punch_packet( + &self.shuffled_port_vec, + listener.clone(), + transaction_id, + &public_ips, + last_port_index, + max_k2 as usize, + ) + .await + .with_context(|| "failed to send symmetric hole punch packet randomly")?; + } + + return Ok(SendPunchPacketHardSymResponse { + next_port_index: next_port_index as u32, + }); + } +} + +pub(crate) struct PunchSymToConeHoleClient { + peer_mgr: Arc, + udp_array: RwLock>>, + try_direct_connect: AtomicBool, + punch_predicablely: AtomicBool, + punch_randomly: AtomicBool, +} + +impl PunchSymToConeHoleClient { + pub(crate) fn new(peer_mgr: Arc) -> Self { + Self { + peer_mgr, + udp_array: RwLock::new(None), + try_direct_connect: AtomicBool::new(true), + punch_predicablely: AtomicBool::new(true), + punch_randomly: AtomicBool::new(true), + } + } + + async fn prepare_udp_array(&self) -> Result, anyhow::Error> { + let rlocked = self.udp_array.read().await; + if let Some(udp_array) = rlocked.clone() { + return Ok(udp_array); + } + + drop(rlocked); + let mut wlocked = self.udp_array.write().await; + if let Some(udp_array) = wlocked.clone() { + return Ok(udp_array); + } + + let udp_array = Arc::new(UdpSocketArray::new( + UDP_ARRAY_SIZE_FOR_HARD_SYM, + self.peer_mgr.get_global_ctx().net_ns.clone(), + )); + udp_array.start().await?; + wlocked.replace(udp_array.clone()); + Ok(udp_array) + } + + pub(crate) async fn clear_udp_array(&self) { + let mut wlocked = self.udp_array.write().await; + wlocked.take(); + } + + async fn get_base_port_for_easy_sym(&self, my_nat_info: UdpNatType) -> Option { + let global_ctx = self.peer_mgr.get_global_ctx(); + if my_nat_info.is_easy_sym() { + match global_ctx + .get_stun_info_collector() + .get_udp_port_mapping(0) + .await + { + Ok(addr) => Some(addr.port()), + ret => { + tracing::warn!(?ret, "failed to get udp port mapping for easy sym"); + None + } + } + } else { + None + } + } + + async fn remote_send_hole_punch_packet_predicable< + S: UdpHolePunchRpc, + >( + rpc_stub: S, + base_port_for_easy_sym: Option, + my_nat_info: UdpNatType, + remote_mapped_addr: crate::proto::common::SocketAddr, + public_ips: Vec, + tid: u32, + ) { + let Some(inc) = my_nat_info.get_inc_of_easy_sym() else { + return; + }; + let req = SendPunchPacketEasySymRequest { + listener_mapped_addr: remote_mapped_addr.clone().into(), + public_ips: public_ips.clone().into_iter().map(|x| x.into()).collect(), + transaction_id: tid, + base_port_num: base_port_for_easy_sym.unwrap() as u32, + max_port_num: 50, + is_incremental: inc, + }; + tracing::debug!(?req, "send punch packet for easy sym start"); + let ret = rpc_stub + .send_punch_packet_easy_sym( + BaseController { + timeout_ms: 4000, + trace_id: 0, + ..Default::default() + }, + req, + ) + .await; + tracing::debug!(?ret, "send punch packet for easy sym return"); + } + + async fn remote_send_hole_punch_packet_random< + S: UdpHolePunchRpc, + >( + rpc_stub: S, + remote_mapped_addr: crate::proto::common::SocketAddr, + public_ips: Vec, + tid: u32, + round: u32, + port_index: u32, + ) -> Option { + let req = SendPunchPacketHardSymRequest { + listener_mapped_addr: remote_mapped_addr.clone().into(), + public_ips: public_ips.clone().into_iter().map(|x| x.into()).collect(), + transaction_id: tid, + round, + port_index, + }; + tracing::debug!(?req, "send punch packet for hard sym start"); + match rpc_stub + .send_punch_packet_hard_sym( + BaseController { + timeout_ms: 4000, + trace_id: 0, + ..Default::default() + }, + req, + ) + .await + { + Err(e) => { + tracing::error!(?e, "failed to send punch packet for hard sym"); + return None; + } + Ok(resp) => return Some(resp.next_port_index), + } + } + + async fn get_rpc_stub( + &self, + dst_peer_id: PeerId, + ) -> Box<(dyn UdpHolePunchRpc + std::marker::Send + 'static)> { + self.peer_mgr + .get_peer_rpc_mgr() + .rpc_client() + .scoped_client::>( + self.peer_mgr.my_peer_id(), + dst_peer_id, + self.peer_mgr.get_global_ctx().get_network_name(), + ) + } + + async fn check_hole_punch_result( + udp_array: &Arc, + packet: &[u8], + tid: u32, + remote_mapped_addr: crate::proto::common::SocketAddr, + scoped_punch_task: &ScopedTask, + ) -> Result>, anyhow::Error> { + // no matter what the result is, we should check if we received any hole punching packet + let mut ret_tunnel: Option> = None; + let mut finish_time: Option = None; + while finish_time.is_none() || finish_time.as_ref().unwrap().elapsed().as_millis() < 1000 { + udp_array + .send_with_all(&packet, remote_mapped_addr.into()) + .await?; + + tokio::time::sleep(Duration::from_millis(200)).await; + + if finish_time.is_none() && (*scoped_punch_task).is_finished() { + finish_time = Some(Instant::now()); + } + + let Some(socket) = udp_array.try_fetch_punched_socket(tid) else { + tracing::debug!("no punched socket found, wait for more time"); + continue; + }; + + // if hole punched but tunnel creation failed, need to retry entire process. + match try_connect_with_socket(socket.socket.clone(), remote_mapped_addr.into()).await { + Ok(tunnel) => { + ret_tunnel.replace(tunnel); + break; + } + Err(e) => { + tracing::error!(?e, "failed to connect with socket"); + udp_array.add_new_socket(socket.socket).await?; + continue; + } + } + } + + Ok(ret_tunnel) + } + + #[tracing::instrument(err(level = Level::ERROR), skip(self))] + pub(crate) async fn do_hole_punching( + &self, + dst_peer_id: PeerId, + round: u32, + last_port_idx: &mut usize, + my_nat_info: UdpNatType, + ) -> Result>, anyhow::Error> { + let udp_array = self.prepare_udp_array().await?; + let global_ctx = self.peer_mgr.get_global_ctx(); + + let rpc_stub = self + .peer_mgr + .get_peer_rpc_mgr() + .rpc_client() + .scoped_client::>( + self.peer_mgr.my_peer_id(), + dst_peer_id, + global_ctx.get_network_name(), + ); + + let resp = rpc_stub + .select_punch_listener( + BaseController::default(), + SelectPunchListenerRequest { force_new: false }, + ) + .await + .with_context(|| "failed to select punch listener")?; + let remote_mapped_addr = resp.listener_mapped_addr.ok_or(anyhow::anyhow!( + "select_punch_listener response missing listener_mapped_addr" + ))?; + + // try direct connect first + if self.try_direct_connect.load(Ordering::Relaxed) { + if let Ok(tunnel) = try_connect_with_socket( + Arc::new(UdpSocket::bind("0.0.0.0:0").await?), + remote_mapped_addr.into(), + ) + .await + { + return Ok(Some(tunnel)); + } + } + + let stun_info = global_ctx.get_stun_info_collector().get_stun_info(); + let public_ips: Vec = stun_info + .public_ip + .iter() + .map(|x| x.parse().unwrap()) + .collect(); + if public_ips.is_empty() { + return Err(anyhow::anyhow!("failed to get public ips")); + } + + let tid = rand::thread_rng().gen(); + let packet = new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes(); + udp_array.add_intreast_tid(tid); + defer! { udp_array.remove_intreast_tid(tid);} + + let port_index = *last_port_idx as u32; + let base_port_for_easy_sym = self.get_base_port_for_easy_sym(my_nat_info).await; + udp_array + .send_with_all(&packet, remote_mapped_addr.into()) + .await?; + + if self.punch_predicablely.load(Ordering::Relaxed) && base_port_for_easy_sym.is_some() { + let rpc_stub = self.get_rpc_stub(dst_peer_id).await; + let scoped_punch_task: ScopedTask<()> = + tokio::spawn(Self::remote_send_hole_punch_packet_predicable( + rpc_stub, + base_port_for_easy_sym, + my_nat_info, + remote_mapped_addr.clone(), + public_ips.clone(), + tid, + )) + .into(); + let ret_tunnel = Self::check_hole_punch_result( + &udp_array, + &packet, + tid, + remote_mapped_addr.clone(), + &scoped_punch_task, + ) + .await?; + + let task_ret = scoped_punch_task.await; + tracing::debug!(?ret_tunnel, ?task_ret, "predictable punch task got result"); + if let Some(tunnel) = ret_tunnel { + return Ok(Some(tunnel)); + } + } + + let rpc_stub = self.get_rpc_stub(dst_peer_id).await; + let scoped_punch_task: ScopedTask> = + tokio::spawn(Self::remote_send_hole_punch_packet_random( + rpc_stub, + remote_mapped_addr.clone(), + public_ips.clone(), + tid, + round, + port_index, + )) + .into(); + let ret_tunnel = Self::check_hole_punch_result( + &udp_array, + &packet, + tid, + remote_mapped_addr.clone(), + &scoped_punch_task, + ) + .await?; + + let punch_task_result = scoped_punch_task.await; + tracing::debug!(?punch_task_result, ?ret_tunnel, "punch task got result"); + + if let Ok(Some(next_port_idx)) = punch_task_result { + *last_port_idx = next_port_idx as usize; + } else { + *last_port_idx = rand::random(); + } + + Ok(ret_tunnel) + } +} + +#[cfg(test)] +pub mod tests { + use std::{ + sync::{atomic::AtomicU32, Arc}, + time::Duration, + }; + + use tokio::net::UdpSocket; + + use crate::{ + connector::udp_hole_punch::{ + tests::create_mock_peer_manager_with_mock_stun, UdpHolePunchConnector, RUN_TESTING, + }, + peers::tests::{connect_peer_manager, wait_route_appear, wait_route_appear_with_cost}, + proto::common::NatType, + tunnel::common::tests::wait_for_condition, + }; + + #[tokio::test] + #[serial_test::serial] + #[serial_test::serial(hole_punch)] + async fn hole_punching_symmetric_only_random() { + RUN_TESTING.store(true, std::sync::atomic::Ordering::Relaxed); + + let p_a = create_mock_peer_manager_with_mock_stun(NatType::Symmetric).await; + let p_b = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await; + let p_c = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await; + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_b.clone(), p_c.clone()).await; + wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap(); + + let mut hole_punching_a = UdpHolePunchConnector::new(p_a.clone()); + let mut hole_punching_c = UdpHolePunchConnector::new(p_c.clone()); + + hole_punching_a + .client + .data() + .sym_to_cone_client + .try_direct_connect + .store(false, std::sync::atomic::Ordering::Relaxed); + + hole_punching_a + .client + .data() + .sym_to_cone_client + .punch_predicablely + .store(false, std::sync::atomic::Ordering::Relaxed); + + hole_punching_a.run().await.unwrap(); + hole_punching_c.run().await.unwrap(); + + hole_punching_a.client.run_immediately().await; + + wait_for_condition( + || async { + hole_punching_a + .client + .data() + .sym_to_cone_client + .udp_array + .read() + .await + .is_some() + }, + Duration::from_secs(5), + ) + .await; + + println!("start punching {:?}", p_a.list_routes().await); + + wait_for_condition( + || async { + wait_route_appear_with_cost(p_a.clone(), p_c.my_peer_id(), Some(1)) + .await + .is_ok() + }, + Duration::from_secs(10), + ) + .await; + println!("{:?}", p_a.list_routes().await); + + wait_for_condition( + || async { + hole_punching_a + .client + .data() + .sym_to_cone_client + .udp_array + .read() + .await + .is_none() + }, + Duration::from_secs(10), + ) + .await; + } + + #[rstest::rstest] + #[tokio::test] + #[serial_test::serial(hole_punch)] + async fn hole_punching_symmetric_only_predict(#[values("true", "false")] is_inc: bool) { + RUN_TESTING.store(true, std::sync::atomic::Ordering::Relaxed); + + let p_a = create_mock_peer_manager_with_mock_stun(if is_inc { + NatType::SymmetricEasyInc + } else { + NatType::SymmetricEasyDec + }) + .await; + let p_b = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await; + let p_c = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await; + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_b.clone(), p_c.clone()).await; + wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap(); + + let mut hole_punching_a = UdpHolePunchConnector::new(p_a.clone()); + let mut hole_punching_c = UdpHolePunchConnector::new(p_c.clone()); + + hole_punching_a + .client + .data() + .sym_to_cone_client + .try_direct_connect + .store(false, std::sync::atomic::Ordering::Relaxed); + + hole_punching_a + .client + .data() + .sym_to_cone_client + .punch_randomly + .store(false, std::sync::atomic::Ordering::Relaxed); + + hole_punching_a.run().await.unwrap(); + hole_punching_c.run().await.unwrap(); + + let udps = if is_inc { + let udp1 = Arc::new(UdpSocket::bind("0.0.0.0:40147").await.unwrap()); + let udp2 = Arc::new(UdpSocket::bind("0.0.0.0:40194").await.unwrap()); + vec![udp1, udp2] + } else { + let udp1 = Arc::new(UdpSocket::bind("0.0.0.0:40141").await.unwrap()); + let udp2 = Arc::new(UdpSocket::bind("0.0.0.0:40100").await.unwrap()); + vec![udp1, udp2] + }; + // let udp_dec = Arc::new(UdpSocket::bind("0.0.0.0:40140").await.unwrap()); + // let udp_dec2 = Arc::new(UdpSocket::bind("0.0.0.0:40050").await.unwrap()); + + let counter = Arc::new(AtomicU32::new(0)); + + // all these sockets should receive hole punching packet + for udp in udps.iter().map(Arc::clone) { + let counter = counter.clone(); + tokio::spawn(async move { + let mut buf = [0u8; 1024]; + let (len, addr) = udp.recv_from(&mut buf).await.unwrap(); + println!( + "got predictable punch packet, {:?} {:?} {:?}", + len, + addr, + udp.local_addr() + ); + counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + }); + } + + hole_punching_a.client.run_immediately().await; + + let udp_len = udps.len(); + wait_for_condition( + || async { counter.load(std::sync::atomic::Ordering::Relaxed) == udp_len as u32 }, + Duration::from_secs(30), + ) + .await; + } +} diff --git a/rust/easytier/src/easytier-cli.rs b/rust/easytier/src/easytier-cli.rs new file mode 100644 index 0000000..0b16a89 --- /dev/null +++ b/rust/easytier/src/easytier-cli.rs @@ -0,0 +1,1379 @@ +use std::{ + ffi::OsString, fmt::Write, net::SocketAddr, path::PathBuf, sync::Mutex, time::Duration, vec, +}; + +use anyhow::{Context, Ok}; +use clap::{command, Args, Parser, Subcommand}; +use humansize::format_size; +use service_manager::*; +use tabled::settings::Style; +use tokio::time::timeout; + +use easytier::{ + common::{ + constants::EASYTIER_VERSION, + stun::{StunInfoCollector, StunInfoCollectorTrait}, + }, + proto::{ + cli::{ + list_peer_route_pair, ConnectorManageRpc, ConnectorManageRpcClientFactory, + DumpRouteRequest, GetVpnPortalInfoRequest, ListConnectorRequest, + ListForeignNetworkRequest, ListGlobalForeignNetworkRequest, ListPeerRequest, + ListPeerResponse, ListRouteRequest, ListRouteResponse, NodeInfo, PeerManageRpc, + PeerManageRpcClientFactory, ShowNodeInfoRequest, TcpProxyEntryState, + TcpProxyEntryTransportType, TcpProxyRpc, TcpProxyRpcClientFactory, VpnPortalRpc, + VpnPortalRpcClientFactory, + }, + common::NatType, + peer_rpc::{GetGlobalPeerMapRequest, PeerCenterRpc, PeerCenterRpcClientFactory}, + rpc_impl::standalone::StandAloneClient, + rpc_types::controller::BaseController, + }, + tunnel::tcp::TcpTunnelConnector, + utils::{cost_to_str, float_to_str, PeerRoutePair}, +}; + +rust_i18n::i18n!("locales", fallback = "en"); + +#[derive(Parser, Debug)] +#[command(name = "easytier-cli", author, version = EASYTIER_VERSION, about, long_about = None)] +struct Cli { + /// the instance name + #[arg(short = 'p', long, default_value = "127.0.0.1:15888")] + rpc_portal: SocketAddr, + + #[arg(short, long, default_value = "false", help = "verbose output")] + verbose: bool, + + #[command(subcommand)] + sub_command: SubCommand, +} + +#[derive(Subcommand, Debug)] +enum SubCommand { + #[command(about = "show peers info")] + Peer(PeerArgs), + #[command(about = "manage connectors")] + Connector(ConnectorArgs), + #[command(about = "do stun test")] + Stun, + #[command(about = "show route info")] + Route(RouteArgs), + #[command(about = "show global peers info")] + PeerCenter, + #[command(about = "show vpn portal (wireguard) info")] + VpnPortal, + #[command(about = "inspect self easytier-core status")] + Node(NodeArgs), + #[command(about = "manage easytier-core as a system service")] + Service(ServiceArgs), + #[command(about = "show tcp/kcp proxy status")] + Proxy, +} + +#[derive(Args, Debug)] +struct PeerArgs { + #[command(subcommand)] + sub_command: Option, +} + +#[derive(Args, Debug)] +struct PeerListArgs { + #[arg(short, long)] + verbose: bool, +} + +#[derive(Subcommand, Debug)] +enum PeerSubCommand { + Add, + Remove, + List(PeerListArgs), + ListForeign, + ListGlobalForeign, +} + +#[derive(Args, Debug)] +struct RouteArgs { + #[command(subcommand)] + sub_command: Option, +} + +#[derive(Subcommand, Debug)] +enum RouteSubCommand { + List, + Dump, +} + +#[derive(Args, Debug)] +struct ConnectorArgs { + #[arg(short, long)] + ipv4: Option, + + #[arg(short, long)] + peers: Vec, + + #[command(subcommand)] + sub_command: Option, +} + +#[derive(Subcommand, Debug)] +enum ConnectorSubCommand { + Add, + Remove, + List, +} + +#[derive(Subcommand, Debug)] +enum NodeSubCommand { + #[command(about = "show node info")] + Info, + #[command(about = "show node config")] + Config, +} + +#[derive(Args, Debug)] +struct NodeArgs { + #[command(subcommand)] + sub_command: Option, +} + +#[derive(Args, Debug)] +struct ServiceArgs { + #[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")] + name: String, + + #[command(subcommand)] + sub_command: ServiceSubCommand, +} + +#[derive(Subcommand, Debug)] +enum ServiceSubCommand { + #[command(about = "register easytier-core as a system service")] + Install(InstallArgs), + #[command(about = "unregister easytier-core system service")] + Uninstall, + #[command(about = "check easytier-core system service status")] + Status, + #[command(about = "start easytier-core system service")] + Start, + #[command(about = "stop easytier-core system service")] + Stop, +} + +#[derive(Args, Debug)] +struct InstallArgs { + #[arg(long, default_value = env!("CARGO_PKG_DESCRIPTION"), help = "service description")] + description: String, + + #[arg(long)] + display_name: Option, + + #[arg(long, default_value = "false")] + disable_autostart: bool, + + #[arg(long, help = "path to easytier-core binary")] + core_path: Option, + + #[arg(long)] + service_work_dir: Option, + + #[arg( + trailing_var_arg = true, + allow_hyphen_values = true, + help = "args to pass to easytier-core" + )] + core_args: Option>, +} + +type Error = anyhow::Error; + +struct CommandHandler { + client: Mutex, + verbose: bool, +} + +type RpcClient = StandAloneClient; + +impl CommandHandler { + async fn get_peer_manager_client( + &self, + ) -> Result>, Error> { + Ok(self + .client + .lock() + .unwrap() + .scoped_client::>("".to_string()) + .await + .with_context(|| "failed to get peer manager client")?) + } + + async fn get_connector_manager_client( + &self, + ) -> Result>, Error> { + Ok(self + .client + .lock() + .unwrap() + .scoped_client::>("".to_string()) + .await + .with_context(|| "failed to get connector manager client")?) + } + + async fn get_peer_center_client( + &self, + ) -> Result>, Error> { + Ok(self + .client + .lock() + .unwrap() + .scoped_client::>("".to_string()) + .await + .with_context(|| "failed to get peer center client")?) + } + + async fn get_vpn_portal_client( + &self, + ) -> Result>, Error> { + Ok(self + .client + .lock() + .unwrap() + .scoped_client::>("".to_string()) + .await + .with_context(|| "failed to get vpn portal client")?) + } + + async fn get_tcp_proxy_client( + &self, + transport_type: &str, + ) -> Result>, Error> { + Ok(self + .client + .lock() + .unwrap() + .scoped_client::>(transport_type.to_string()) + .await + .with_context(|| "failed to get vpn portal client")?) + } + + async fn list_peers(&self) -> Result { + let client = self.get_peer_manager_client().await?; + let request = ListPeerRequest::default(); + let response = client.list_peer(BaseController::default(), request).await?; + Ok(response) + } + + async fn list_routes(&self) -> Result { + let client = self.get_peer_manager_client().await?; + let request = ListRouteRequest::default(); + let response = client + .list_route(BaseController::default(), request) + .await?; + Ok(response) + } + + async fn list_peer_route_pair(&self) -> Result, Error> { + let peers = self.list_peers().await?.peer_infos; + let routes = self.list_routes().await?.routes; + Ok(list_peer_route_pair(peers, routes)) + } + + #[allow(dead_code)] + fn handle_peer_add(&self, _args: PeerArgs) { + println!("add peer"); + } + + #[allow(dead_code)] + fn handle_peer_remove(&self, _args: PeerArgs) { + println!("remove peer"); + } + + async fn handle_peer_list(&self, _args: &PeerArgs) -> Result<(), Error> { + #[derive(tabled::Tabled)] + struct PeerTableItem { + ipv4: String, + hostname: String, + cost: String, + lat_ms: String, + loss_rate: String, + rx_bytes: String, + tx_bytes: String, + tunnel_proto: String, + nat_type: String, + id: String, + version: String, + } + + impl From for PeerTableItem { + fn from(p: PeerRoutePair) -> Self { + let route = p.route.clone().unwrap_or_default(); + PeerTableItem { + ipv4: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(), + hostname: route.hostname.clone(), + cost: cost_to_str(route.cost), + lat_ms: float_to_str(p.get_latency_ms().unwrap_or(0.0), 3), + loss_rate: float_to_str(p.get_loss_rate().unwrap_or(0.0), 3), + rx_bytes: format_size(p.get_rx_bytes().unwrap_or(0), humansize::DECIMAL), + tx_bytes: format_size(p.get_tx_bytes().unwrap_or(0), humansize::DECIMAL), + tunnel_proto: p + .get_conn_protos() + .unwrap_or_default() + .join(",") + .to_string(), + nat_type: p.get_udp_nat_type(), + id: route.peer_id.to_string(), + version: if route.version.is_empty() { + "unknown".to_string() + } else { + route.version.to_string() + }, + } + } + } + + impl From for PeerTableItem { + fn from(p: NodeInfo) -> Self { + PeerTableItem { + ipv4: p.ipv4_addr.clone(), + hostname: p.hostname.clone(), + cost: "Local".to_string(), + lat_ms: "-".to_string(), + loss_rate: "-".to_string(), + rx_bytes: "-".to_string(), + tx_bytes: "-".to_string(), + tunnel_proto: "-".to_string(), + nat_type: if let Some(info) = p.stun_info { + info.udp_nat_type().as_str_name().to_string() + } else { + "Unknown".to_string() + }, + id: p.peer_id.to_string(), + version: p.version, + } + } + } + + let mut items: Vec = vec![]; + let peer_routes = self.list_peer_route_pair().await?; + if self.verbose { + println!("{:#?}", peer_routes); + return Ok(()); + } + + let client = self.get_peer_manager_client().await?; + let node_info = client + .show_node_info(BaseController::default(), ShowNodeInfoRequest::default()) + .await? + .node_info + .ok_or(anyhow::anyhow!("node info not found"))?; + items.push(node_info.into()); + + for p in peer_routes { + items.push(p.into()); + } + + println!("{}", tabled::Table::new(items).with(Style::modern())); + + Ok(()) + } + + async fn handle_route_dump(&self) -> Result<(), Error> { + let client = self.get_peer_manager_client().await?; + let request = DumpRouteRequest::default(); + let response = client + .dump_route(BaseController::default(), request) + .await?; + println!("response: {}", response.result); + Ok(()) + } + + async fn handle_foreign_network_list(&self) -> Result<(), Error> { + let client = self.get_peer_manager_client().await?; + let request = ListForeignNetworkRequest::default(); + let response = client + .list_foreign_network(BaseController::default(), request) + .await?; + let network_map = response; + if self.verbose { + println!("{:#?}", network_map); + return Ok(()); + } + + for (idx, (k, v)) in network_map.foreign_networks.iter().enumerate() { + println!("{} Network Name: {}", idx + 1, k); + for peer in v.peers.iter() { + println!( + " peer_id: {}, peer_conn_count: {}, conns: [ {} ]", + peer.peer_id, + peer.conns.len(), + peer.conns + .iter() + .map(|conn| format!( + "remote_addr: {}, rx_bytes: {}, tx_bytes: {}, latency_us: {}", + conn.tunnel + .as_ref() + .map(|t| t.remote_addr.clone().unwrap_or_default()) + .unwrap_or_default(), + conn.stats.as_ref().map(|s| s.rx_bytes).unwrap_or_default(), + conn.stats.as_ref().map(|s| s.tx_bytes).unwrap_or_default(), + conn.stats + .as_ref() + .map(|s| s.latency_us) + .unwrap_or_default(), + )) + .collect::>() + .join("; ") + ); + } + } + Ok(()) + } + + async fn handle_global_foreign_network_list(&self) -> Result<(), Error> { + let client = self.get_peer_manager_client().await?; + let request = ListGlobalForeignNetworkRequest::default(); + let response = client + .list_global_foreign_network(BaseController::default(), request) + .await?; + if self.verbose { + println!("{:#?}", response); + return Ok(()); + } + + for (k, v) in response.foreign_networks.iter() { + println!("Peer ID: {}", k); + for n in v.foreign_networks.iter() { + println!( + " Network Name: {}, Last Updated: {}, Version: {}, PeerIds: {:?}", + n.network_name, n.last_updated, n.version, n.peer_ids + ); + } + } + + Ok(()) + } + + async fn handle_route_list(&self) -> Result<(), Error> { + #[derive(tabled::Tabled)] + struct RouteTableItem { + ipv4: String, + hostname: String, + proxy_cidrs: String, + + next_hop_ipv4: String, + next_hop_hostname: String, + next_hop_lat: f64, + path_len: i32, + path_latency: i32, + + next_hop_ipv4_lat_first: String, + next_hop_hostname_lat_first: String, + path_len_lat_first: i32, + path_latency_lat_first: i32, + + version: String, + } + + let mut items: Vec = vec![]; + let client = self.get_peer_manager_client().await?; + let node_info = client + .show_node_info(BaseController::default(), ShowNodeInfoRequest::default()) + .await? + .node_info + .ok_or(anyhow::anyhow!("node info not found"))?; + + items.push(RouteTableItem { + ipv4: node_info.ipv4_addr.clone(), + hostname: node_info.hostname.clone(), + proxy_cidrs: node_info.proxy_cidrs.join(", "), + + next_hop_ipv4: "-".to_string(), + next_hop_hostname: "Local".to_string(), + next_hop_lat: 0.0, + path_len: 0, + path_latency: 0, + + next_hop_ipv4_lat_first: "-".to_string(), + next_hop_hostname_lat_first: "Local".to_string(), + path_len_lat_first: 0, + path_latency_lat_first: 0, + + version: node_info.version.clone(), + }); + let peer_routes = self.list_peer_route_pair().await?; + for p in peer_routes.iter() { + let Some(next_hop_pair) = peer_routes.iter().find(|pair| { + pair.route.clone().unwrap_or_default().peer_id + == p.route.clone().unwrap_or_default().next_hop_peer_id + }) else { + continue; + }; + + let next_hop_pair_latency_first = peer_routes.iter().find(|pair| { + pair.route.clone().unwrap_or_default().peer_id + == p.route + .clone() + .unwrap_or_default() + .next_hop_peer_id_latency_first + .unwrap_or_default() + }); + + let route = p.route.clone().unwrap_or_default(); + if route.cost == 1 { + items.push(RouteTableItem { + ipv4: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(), + hostname: route.hostname.clone(), + proxy_cidrs: route.proxy_cidrs.clone().join(",").to_string(), + + next_hop_ipv4: "DIRECT".to_string(), + next_hop_hostname: "".to_string(), + next_hop_lat: next_hop_pair.get_latency_ms().unwrap_or(0.0), + path_len: route.cost, + path_latency: next_hop_pair.get_latency_ms().unwrap_or_default() as i32, + + next_hop_ipv4_lat_first: next_hop_pair_latency_first + .map(|pair| pair.route.clone().unwrap_or_default().ipv4_addr) + .unwrap_or_default() + .map(|ip| ip.to_string()) + .unwrap_or_default(), + next_hop_hostname_lat_first: next_hop_pair_latency_first + .map(|pair| pair.route.clone().unwrap_or_default().hostname) + .unwrap_or_default() + .clone(), + path_latency_lat_first: next_hop_pair_latency_first + .map(|pair| { + pair.route + .clone() + .unwrap_or_default() + .path_latency_latency_first + .unwrap_or_default() + }) + .unwrap_or_default(), + path_len_lat_first: next_hop_pair_latency_first + .map(|pair| { + pair.route + .clone() + .unwrap_or_default() + .cost_latency_first + .unwrap_or_default() + }) + .unwrap_or_default(), + + version: if route.version.is_empty() { + "unknown".to_string() + } else { + route.version.to_string() + }, + }); + } else { + items.push(RouteTableItem { + ipv4: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(), + hostname: route.hostname.clone(), + proxy_cidrs: route.proxy_cidrs.clone().join(",").to_string(), + next_hop_ipv4: next_hop_pair + .route + .clone() + .unwrap_or_default() + .ipv4_addr + .map(|ip| ip.to_string()) + .unwrap_or_default(), + next_hop_hostname: next_hop_pair + .route + .clone() + .unwrap_or_default() + .hostname + .clone(), + next_hop_lat: next_hop_pair.get_latency_ms().unwrap_or(0.0), + path_len: route.cost, + path_latency: p.route.clone().unwrap_or_default().path_latency as i32, + + next_hop_ipv4_lat_first: next_hop_pair_latency_first + .map(|pair| pair.route.clone().unwrap_or_default().ipv4_addr) + .unwrap_or_default() + .map(|ip| ip.to_string()) + .unwrap_or_default(), + next_hop_hostname_lat_first: next_hop_pair_latency_first + .map(|pair| pair.route.clone().unwrap_or_default().hostname) + .unwrap_or_default() + .clone(), + path_latency_lat_first: next_hop_pair_latency_first + .map(|pair| { + pair.route + .clone() + .unwrap_or_default() + .path_latency_latency_first + .unwrap_or_default() + }) + .unwrap_or_default(), + path_len_lat_first: next_hop_pair_latency_first + .map(|pair| { + pair.route + .clone() + .unwrap_or_default() + .cost_latency_first + .unwrap_or_default() + }) + .unwrap_or_default(), + + version: if route.version.is_empty() { + "unknown".to_string() + } else { + route.version.to_string() + }, + }); + } + } + + println!("{}", tabled::Table::new(items).with(Style::modern())); + + Ok(()) + } + + async fn handle_connector_list(&self) -> Result<(), Error> { + let client = self.get_connector_manager_client().await?; + let request = ListConnectorRequest::default(); + let response = client + .list_connector(BaseController::default(), request) + .await?; + println!("response: {:#?}", response); + Ok(()) + } +} + +pub struct ServiceInstallOptions { + pub program: PathBuf, + pub args: Vec, + pub work_directory: PathBuf, + pub disable_autostart: bool, + pub description: Option, + pub display_name: Option, +} +pub struct Service { + lable: ServiceLabel, + kind: ServiceManagerKind, + service_manager: Box, +} + +impl Service { + pub fn new(name: String) -> Result { + #[cfg(target_os = "windows")] + let service_manager = Box::new(crate::win_service_manager::WinServiceManager::new()?); + + #[cfg(not(target_os = "windows"))] + let service_manager = ::native()?; + let kind = ServiceManagerKind::native()?; + + Ok(Self { + lable: name.parse()?, + kind, + service_manager, + }) + } + + pub fn install(&self, options: &ServiceInstallOptions) -> Result<(), Error> { + let ctx = ServiceInstallCtx { + label: self.lable.clone(), + program: options.program.clone(), + args: options.args.clone(), + contents: self.make_install_content_option(options), + autostart: !options.disable_autostart, + username: None, + working_directory: Some(options.work_directory.clone()), + environment: None, + }; + if self.status()? != ServiceStatus::NotInstalled { + return Err(anyhow::anyhow!( + "Service is already installed! Service Name: {}", + self.lable + )); + } + + self.service_manager + .install(ctx.clone()) + .map_err(|e| anyhow::anyhow!("failed to install service: {:?}", e))?; + + println!( + "Service installed successfully! Service Name: {}", + self.lable + ); + + Ok(()) + } + + pub fn uninstall(&self) -> Result<(), Error> { + let ctx = ServiceUninstallCtx { + label: self.lable.clone(), + }; + let status = self.status()?; + + if status == ServiceStatus::NotInstalled { + return Err(anyhow::anyhow!("Service is not installed")); + } + + if status == ServiceStatus::Running { + self.service_manager.stop(ServiceStopCtx { + label: self.lable.clone(), + })?; + } + + self.service_manager + .uninstall(ctx) + .map_err(|e| anyhow::anyhow!("failed to uninstall service: {}", e)) + } + + pub fn status(&self) -> Result { + let ctx = ServiceStatusCtx { + label: self.lable.clone(), + }; + let status = self.service_manager.status(ctx)?; + + Ok(status) + } + + pub fn start(&self) -> Result<(), Error> { + let ctx = ServiceStartCtx { + label: self.lable.clone(), + }; + let status = self.status()?; + + match status { + ServiceStatus::Running => Err(anyhow::anyhow!("Service is already running")), + ServiceStatus::Stopped(_) => { + self.service_manager + .start(ctx) + .map_err(|e| anyhow::anyhow!("failed to start service: {}", e))?; + Ok(()) + } + ServiceStatus::NotInstalled => Err(anyhow::anyhow!("Service is not installed")), + } + } + + pub fn stop(&self) -> Result<(), Error> { + let ctx = ServiceStopCtx { + label: self.lable.clone(), + }; + let status = self.status()?; + + match status { + ServiceStatus::Running => { + self.service_manager + .stop(ctx) + .map_err(|e| anyhow::anyhow!("failed to stop service: {}", e))?; + Ok(()) + } + ServiceStatus::Stopped(_) => Err(anyhow::anyhow!("Service is already stopped")), + ServiceStatus::NotInstalled => Err(anyhow::anyhow!("Service is not installed")), + } + } + + fn make_install_content_option(&self, options: &ServiceInstallOptions) -> Option { + match self.kind { + ServiceManagerKind::Systemd => Some(self.make_systemd_unit(options).unwrap()), + ServiceManagerKind::Rcd => Some(self.make_rcd_script(options).unwrap()), + ServiceManagerKind::OpenRc => Some(self.make_open_rc_script(options).unwrap()), + _ => { + #[cfg(target_os = "windows")] + { + let win_options = win_service_manager::WinServiceInstallOptions { + description: options.description.clone(), + display_name: options.display_name.clone(), + dependencies: Some(vec!["rpcss".to_string(), "dnscache".to_string()]), + }; + + Some(serde_json::to_string(&win_options).unwrap()) + } + + #[cfg(not(target_os = "windows"))] + None + } + } + } + + fn make_systemd_unit( + &self, + options: &ServiceInstallOptions, + ) -> Result { + let args = options + .args + .iter() + .map(|a| a.to_string_lossy()) + .collect::>() + .join(" "); + let target_app = options.program.display().to_string(); + let work_dir = options.work_directory.display().to_string(); + let mut unit_content = String::new(); + + writeln!(unit_content, "[Unit]")?; + writeln!(unit_content, "After=network.target syslog.target")?; + if let Some(ref d) = options.description { + writeln!(unit_content, "Description={d}")?; + } + writeln!(unit_content, "StartLimitIntervalSec=0")?; + writeln!(unit_content)?; + writeln!(unit_content, "[Service]")?; + writeln!(unit_content, "Type=simple")?; + writeln!(unit_content, "WorkingDirectory={work_dir}")?; + writeln!(unit_content, "ExecStart={target_app} {args}")?; + writeln!(unit_content, "Restart=always")?; + writeln!(unit_content, "RestartSec=1")?; + writeln!(unit_content, "LimitNOFILE=infinity")?; + writeln!(unit_content)?; + writeln!(unit_content, "[Install]")?; + writeln!(unit_content, "WantedBy=multi-user.target")?; + + std::result::Result::Ok(unit_content) + } + + fn make_rcd_script(&self, options: &ServiceInstallOptions) -> Result { + let name = self.lable.to_qualified_name(); + let args = options + .args + .iter() + .map(|a| a.to_string_lossy()) + .collect::>() + .join(" "); + let target_app = options.program.display().to_string(); + let work_dir = options.work_directory.display().to_string(); + let mut script = String::new(); + + writeln!(script, "#!/bin/sh")?; + writeln!(script, "#")?; + writeln!(script, "# PROVIDE: {name}")?; + writeln!(script, "# REQUIRE: LOGIN FILESYSTEMS NETWORKING ")?; + writeln!(script, "# KEYWORD: shutdown")?; + writeln!(script)?; + writeln!(script, ". /etc/rc.subr")?; + writeln!(script)?; + writeln!(script, "name=\"{name}\"")?; + if let Some(ref d) = options.description { + writeln!(script, "desc=\"{d}\"")?; + } + writeln!(script, "rcvar=\"{name}_enable\"")?; + writeln!(script)?; + writeln!(script, "load_rc_config ${{name}}")?; + writeln!(script)?; + writeln!(script, ": ${{{name}_options=\"{args}\"}}")?; + writeln!(script)?; + writeln!(script, "{name}_chdir=\"{work_dir}\"")?; + writeln!(script, "pidfile=\"/var/run/${{name}}.pid\"")?; + writeln!(script, "procname=\"{target_app}\"")?; + writeln!(script, "command=\"/usr/sbin/daemon\"")?; + writeln!( + script, + "command_args=\"-c -S -T ${{name}} -p ${{pidfile}} ${{procname}} ${{{name}_options}}\"" + )?; + writeln!(script)?; + writeln!(script, "run_rc_command \"$1\"")?; + + std::result::Result::Ok(script) + } + + fn make_open_rc_script( + &self, + options: &ServiceInstallOptions, + ) -> Result { + let args = options + .args + .iter() + .map(|a| a.to_string_lossy()) + .collect::>() + .join(" "); + let target_app = options.program.display().to_string(); + let work_dir = options.work_directory.display().to_string(); + let mut script = String::new(); + + writeln!(script, "#!/sbin/openrc-run")?; + writeln!(script)?; + if let Some(ref d) = options.description { + writeln!(script, "description=\"{d}\"")?; + } + writeln!(script, "command=\"{target_app}\"")?; + writeln!(script, "command_args=\"{args}\"")?; + writeln!(script, "pidfile=\"/run/${{RC_SVCNAME}}.pid\"")?; + writeln!(script, "command_background=\"yes\"")?; + writeln!(script, "directory=\"{work_dir}\"")?; + writeln!(script)?; + writeln!(script, "depend() {{")?; + writeln!(script, " need net")?; + writeln!(script, " use looger")?; + writeln!(script, "}}")?; + + std::result::Result::Ok(script) + } +} + +#[tokio::main] +#[tracing::instrument] +async fn main() -> Result<(), Error> { + let cli = Cli::parse(); + let client = RpcClient::new(TcpTunnelConnector::new( + format!("tcp://{}:{}", cli.rpc_portal.ip(), cli.rpc_portal.port()) + .parse() + .unwrap(), + )); + let handler = CommandHandler { + client: Mutex::new(client), + verbose: cli.verbose, + }; + + match cli.sub_command { + SubCommand::Peer(peer_args) => match &peer_args.sub_command { + Some(PeerSubCommand::Add) => { + println!("add peer"); + } + Some(PeerSubCommand::Remove) => { + println!("remove peer"); + } + Some(PeerSubCommand::List(arg)) => { + if arg.verbose { + println!("{:#?}", handler.list_peer_route_pair().await?); + } else { + handler.handle_peer_list(&peer_args).await?; + } + } + Some(PeerSubCommand::ListForeign) => { + handler.handle_foreign_network_list().await?; + } + Some(PeerSubCommand::ListGlobalForeign) => { + handler.handle_global_foreign_network_list().await?; + } + None => { + handler.handle_peer_list(&peer_args).await?; + } + }, + SubCommand::Connector(conn_args) => match conn_args.sub_command { + Some(ConnectorSubCommand::Add) => { + println!("add connector"); + } + Some(ConnectorSubCommand::Remove) => { + println!("remove connector"); + } + Some(ConnectorSubCommand::List) => { + handler.handle_connector_list().await?; + } + None => { + handler.handle_connector_list().await?; + } + }, + SubCommand::Route(route_args) => match route_args.sub_command { + Some(RouteSubCommand::List) | None => handler.handle_route_list().await?, + Some(RouteSubCommand::Dump) => handler.handle_route_dump().await?, + }, + SubCommand::Stun => { + timeout(Duration::from_secs(25), async move { + let collector = StunInfoCollector::new_with_default_servers(); + loop { + let ret = collector.get_stun_info(); + if ret.udp_nat_type != NatType::Unknown as i32 { + println!("stun info: {:#?}", ret); + break; + } + tokio::time::sleep(Duration::from_millis(200)).await; + } + }) + .await + .unwrap(); + } + SubCommand::PeerCenter => { + let peer_center_client = handler.get_peer_center_client().await?; + let resp = peer_center_client + .get_global_peer_map( + BaseController::default(), + GetGlobalPeerMapRequest::default(), + ) + .await?; + + #[derive(tabled::Tabled)] + struct PeerCenterTableItem { + node_id: String, + direct_peers: String, + } + + let mut table_rows = vec![]; + for (k, v) in resp.global_peer_map.iter() { + let node_id = k; + let direct_peers = v + .direct_peers + .iter() + .map(|(k, v)| format!("{}: {:?}ms", k, v.latency_ms,)) + .collect::>(); + table_rows.push(PeerCenterTableItem { + node_id: node_id.to_string(), + direct_peers: direct_peers.join("\n"), + }); + } + + println!("{}", tabled::Table::new(table_rows).with(Style::modern())); + } + SubCommand::VpnPortal => { + let vpn_portal_client = handler.get_vpn_portal_client().await?; + let resp = vpn_portal_client + .get_vpn_portal_info( + BaseController::default(), + GetVpnPortalInfoRequest::default(), + ) + .await? + .vpn_portal_info + .unwrap_or_default(); + println!("portal_name: {}", resp.vpn_type); + println!( + r#" +############### client_config_start ############### +{} +############### client_config_end ############### +"#, + resp.client_config + ); + println!("connected_clients:\n{:#?}", resp.connected_clients); + } + SubCommand::Node(sub_cmd) => { + let client = handler.get_peer_manager_client().await?; + let node_info = client + .show_node_info(BaseController::default(), ShowNodeInfoRequest::default()) + .await? + .node_info + .ok_or(anyhow::anyhow!("node info not found"))?; + match sub_cmd.sub_command { + Some(NodeSubCommand::Info) | None => { + let stun_info = node_info.stun_info.clone().unwrap_or_default(); + + let mut builder = tabled::builder::Builder::default(); + builder.push_record(vec!["Virtual IP", node_info.ipv4_addr.as_str()]); + builder.push_record(vec!["Hostname", node_info.hostname.as_str()]); + builder.push_record(vec![ + "Proxy CIDRs", + node_info.proxy_cidrs.join(", ").as_str(), + ]); + builder.push_record(vec!["Peer ID", node_info.peer_id.to_string().as_str()]); + builder.push_record(vec!["Public IP", stun_info.public_ip.join(", ").as_str()]); + builder.push_record(vec![ + "UDP Stun Type", + format!("{:?}", stun_info.udp_nat_type()).as_str(), + ]); + for (idx, l) in node_info.listeners.iter().enumerate() { + if l.starts_with("ring") { + continue; + } + builder.push_record(vec![format!("Listener {}", idx).as_str(), l]); + } + + println!("{}", builder.build().with(Style::modern())); + } + Some(NodeSubCommand::Config) => { + println!("{}", node_info.config); + } + } + } + SubCommand::Service(service_args) => { + let service = Service::new(service_args.name)?; + match service_args.sub_command { + ServiceSubCommand::Install(install_args) => { + let bin_path = install_args.core_path.unwrap_or_else(|| { + let mut ret = std::env::current_exe() + .unwrap() + .parent() + .unwrap() + .join("easytier-core"); + + if cfg!(target_os = "windows") { + ret.set_extension("exe"); + } + + ret + }); + let bin_path = std::fs::canonicalize(bin_path).map_err(|e| { + anyhow::anyhow!("failed to get easytier core application: {}", e) + })?; + let bin_args = install_args.core_args.unwrap_or_default(); + let work_dir = install_args.service_work_dir.unwrap_or_else(|| { + if cfg!(target_os = "windows") { + bin_path.parent().unwrap().to_path_buf() + } else { + std::env::temp_dir() + } + }); + + let work_dir = std::fs::canonicalize(&work_dir).map_err(|e| { + anyhow::anyhow!( + "failed to get service work directory[{}]: {}", + work_dir.display(), + e + ) + })?; + + if !work_dir.is_dir() { + return Err(anyhow::anyhow!("work directory is not a directory")); + } + + let install_options = ServiceInstallOptions { + program: bin_path, + args: bin_args, + work_directory: work_dir, + disable_autostart: install_args.disable_autostart, + description: Some(install_args.description), + display_name: install_args.display_name, + }; + service.install(&install_options)?; + } + ServiceSubCommand::Uninstall => { + service.uninstall()?; + } + ServiceSubCommand::Status => { + let status = service.status()?; + match status { + ServiceStatus::Running => println!("Service is running"), + ServiceStatus::Stopped(_) => println!("Service is stopped"), + ServiceStatus::NotInstalled => println!("Service is not installed"), + } + } + ServiceSubCommand::Start => { + service.start()?; + } + ServiceSubCommand::Stop => { + service.stop()?; + } + } + } + SubCommand::Proxy => { + let mut entries = vec![]; + let client = handler.get_tcp_proxy_client("tcp").await?; + let ret = client + .list_tcp_proxy_entry(BaseController::default(), Default::default()) + .await; + entries.extend(ret.unwrap_or_default().entries); + + let client = handler.get_tcp_proxy_client("kcp_src").await?; + let ret = client + .list_tcp_proxy_entry(BaseController::default(), Default::default()) + .await; + entries.extend(ret.unwrap_or_default().entries); + + let client = handler.get_tcp_proxy_client("kcp_dst").await?; + let ret = client + .list_tcp_proxy_entry(BaseController::default(), Default::default()) + .await; + entries.extend(ret.unwrap_or_default().entries); + + #[derive(tabled::Tabled)] + struct TableItem { + src: String, + dst: String, + start_time: String, + state: String, + transport_type: String, + } + + let table_rows = entries + .iter() + .map(|e| TableItem { + src: SocketAddr::from(e.src.unwrap_or_default()).to_string(), + dst: SocketAddr::from(e.dst.unwrap_or_default()).to_string(), + start_time: chrono::DateTime::::from_timestamp_millis( + (e.start_time * 1000) as i64, + ) + .unwrap() + .with_timezone(&chrono::Local) + .format("%Y-%m-%d %H:%M:%S") + .to_string(), + state: format!("{:?}", TcpProxyEntryState::try_from(e.state).unwrap()), + transport_type: format!( + "{:?}", + TcpProxyEntryTransportType::try_from(e.transport_type).unwrap() + ), + }) + .collect::>(); + + println!("{}", tabled::Table::new(table_rows).with(Style::modern())); + } + } + + Ok(()) +} + +#[cfg(target_os = "windows")] +mod win_service_manager { + use std::{ffi::OsStr, ffi::OsString, io, path::PathBuf}; + use windows_service::{ + service::{ + ServiceAccess, ServiceDependency, ServiceErrorControl, ServiceInfo, ServiceStartType, + ServiceType, + }, + service_manager::{ServiceManager, ServiceManagerAccess}, + }; + + use service_manager::{ + ServiceInstallCtx, ServiceLevel, ServiceStartCtx, ServiceStatus, ServiceStatusCtx, + ServiceStopCtx, ServiceUninstallCtx, + }; + + use winreg::{enums::*, RegKey}; + + use easytier::common::constants::WIN_SERVICE_WORK_DIR_REG_KEY; + + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize)] + pub struct WinServiceInstallOptions { + pub dependencies: Option>, + pub description: Option, + pub display_name: Option, + } + + pub struct WinServiceManager { + service_manager: ServiceManager, + } + + impl WinServiceManager { + pub fn new() -> Result { + let service_manager = + ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::ALL_ACCESS)?; + Ok(Self { service_manager }) + } + } + impl service_manager::ServiceManager for WinServiceManager { + fn available(&self) -> io::Result { + Ok(true) + } + + fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> { + let start_type_ = if ctx.autostart { + ServiceStartType::AutoStart + } else { + ServiceStartType::OnDemand + }; + let srv_name = OsString::from(ctx.label.to_qualified_name()); + let mut dis_name = srv_name.clone(); + let mut description: Option = None; + let mut dependencies = Vec::::new(); + + if let Some(s) = ctx.contents.as_ref() { + let options: WinServiceInstallOptions = serde_json::from_str(s.as_str()).unwrap(); + if let Some(d) = options.dependencies { + dependencies = d + .iter() + .map(|dep| ServiceDependency::Service(OsString::from(dep.clone()))) + .collect::>(); + } + if let Some(d) = options.description { + description = Some(OsString::from(d)); + } + if let Some(d) = options.display_name { + dis_name = OsString::from(d); + } + } + + let service_info = ServiceInfo { + name: srv_name, + display_name: dis_name, + service_type: ServiceType::OWN_PROCESS, + start_type: start_type_, + error_control: ServiceErrorControl::Normal, + executable_path: ctx.program, + launch_arguments: ctx.args, + dependencies: dependencies.clone(), + account_name: None, + account_password: None, + }; + + let service = self + .service_manager + .create_service(&service_info, ServiceAccess::ALL_ACCESS) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + if let Some(s) = description { + service + .set_description(s.clone()) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + } + + if let Some(work_dir) = ctx.working_directory { + set_service_work_directory(&ctx.label.to_qualified_name(), work_dir)?; + } + + Ok(()) + } + + fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> { + let service = self + .service_manager + .open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + service + .delete() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + } + + fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> { + let service = self + .service_manager + .open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + service + .start(&[] as &[&OsStr]) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + } + + fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> { + let service = self + .service_manager + .open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + _ = service + .stop() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + Ok(()) + } + + fn level(&self) -> ServiceLevel { + ServiceLevel::System + } + + fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> { + match level { + ServiceLevel::System => Ok(()), + _ => Err(io::Error::new( + io::ErrorKind::Other, + "Unsupported service level", + )), + } + } + + fn status(&self, ctx: ServiceStatusCtx) -> io::Result { + let service = match self + .service_manager + .open_service(ctx.label.to_qualified_name(), ServiceAccess::QUERY_STATUS) + { + Ok(s) => s, + Err(e) => { + if let windows_service::Error::Winapi(ref win_err) = e { + if win_err.raw_os_error() == Some(0x424) { + return Ok(ServiceStatus::NotInstalled); + } + } + return Err(io::Error::new(io::ErrorKind::Other, e)); + } + }; + + let status = service + .query_status() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + match status.current_state { + windows_service::service::ServiceState::Stopped => Ok(ServiceStatus::Stopped(None)), + _ => Ok(ServiceStatus::Running), + } + } + } + + fn set_service_work_directory(service_name: &str, work_directory: PathBuf) -> io::Result<()> { + let (reg_key, _) = + RegKey::predef(HKEY_LOCAL_MACHINE).create_subkey(WIN_SERVICE_WORK_DIR_REG_KEY)?; + reg_key + .set_value::(service_name, &work_directory.as_os_str().to_os_string())?; + Ok(()) + } +} diff --git a/rust/easytier/src/easytier-core.rs b/rust/easytier/src/easytier-core.rs new file mode 100644 index 0000000..249000b --- /dev/null +++ b/rust/easytier/src/easytier-core.rs @@ -0,0 +1,933 @@ +#![allow(dead_code)] + +#[macro_use] +extern crate rust_i18n; + +use std::{ + net::{Ipv4Addr, SocketAddr}, + path::PathBuf, + sync::Arc, +}; + +use anyhow::Context; +use clap::Parser; +use tokio::net::TcpSocket; + +use easytier::{ + common::{ + config::{ + ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, NetworkIdentity, PeerConfig, + TomlConfigLoader, VpnPortalConfig, + }, + constants::EASYTIER_VERSION, + global_ctx::{EventBusSubscriber, GlobalCtx, GlobalCtxEvent}, + scoped_task::ScopedTask, + stun::MockStunInfoCollector, + }, + connector::{create_connector_by_url, dns_connector::DNSTunnelConnector}, + launcher, + proto::{ + self, + common::{CompressionAlgoPb, NatType}, + }, + tunnel::PROTO_PORT_OFFSET, + utils::{init_logger, setup_panic_handler}, + web_client, +}; + +#[cfg(target_os = "windows")] +windows_service::define_windows_service!(ffi_service_main, win_service_main); + +#[cfg(feature = "mimalloc")] +use mimalloc_rust::GlobalMiMalloc; + +#[cfg(feature = "mimalloc")] +#[global_allocator] +static GLOBAL_MIMALLOC: GlobalMiMalloc = GlobalMiMalloc; + +#[derive(Parser, Debug)] +#[command(name = "easytier-core", author, version = EASYTIER_VERSION , about, long_about = None)] +struct Cli { + #[arg( + short = 'w', + long, + help = t!("core_clap.config_server").to_string() + )] + config_server: Option, + + #[arg( + short, + long, + help = t!("core_clap.config_file").to_string() + )] + config_file: Option, + + #[arg( + long, + help = t!("core_clap.network_name").to_string(), + default_value = "default" + )] + network_name: String, + + #[arg( + long, + help = t!("core_clap.network_secret").to_string(), + default_value = "" + )] + network_secret: String, + + #[arg( + short, + long, + help = t!("core_clap.ipv4").to_string() + )] + ipv4: Option, + + #[arg( + short, + long, + help = t!("core_clap.dhcp").to_string() + )] + dhcp: bool, + + #[arg( + short, + long, + help = t!("core_clap.peers").to_string(), + num_args = 0.. + )] + peers: Vec, + + #[arg( + short, + long, + help = t!("core_clap.external_node").to_string() + )] + external_node: Option, + + #[arg( + short = 'n', + long, + help = t!("core_clap.proxy_networks").to_string() + )] + proxy_networks: Vec, + + #[arg( + short, + long, + help = t!("core_clap.rpc_portal").to_string(), + default_value = "0" + )] + rpc_portal: String, + + #[arg( + short, + long, + help = t!("core_clap.listeners").to_string(), + default_values_t = ["11010".to_string()], + num_args = 0.. + )] + listeners: Vec, + + #[arg( + long, + help = t!("core_clap.mapped_listeners").to_string(), + num_args = 0.. + )] + mapped_listeners: Vec, + + #[arg( + long, + help = t!("core_clap.no_listener").to_string(), + default_value = "false" + )] + no_listener: bool, + + #[arg( + long, + help = t!("core_clap.console_log_level").to_string() + )] + console_log_level: Option, + + #[arg( + long, + help = t!("core_clap.file_log_level").to_string() + )] + file_log_level: Option, + + #[arg( + long, + help = t!("core_clap.file_log_dir").to_string() + )] + file_log_dir: Option, + + #[arg( + long, + help = t!("core_clap.hostname").to_string() + )] + hostname: Option, + + #[arg( + short = 'm', + long, + help = t!("core_clap.instance_name").to_string(), + default_value = "default" + )] + instance_name: String, + + #[arg( + long, + help = t!("core_clap.vpn_portal").to_string() + )] + vpn_portal: Option, + + #[arg( + long, + help = t!("core_clap.default_protocol").to_string() + )] + default_protocol: Option, + + #[arg( + short = 'u', + long, + help = t!("core_clap.disable_encryption").to_string(), + default_value = "false" + )] + disable_encryption: bool, + + #[arg( + long, + help = t!("core_clap.multi_thread").to_string(), + default_value = "true" + )] + multi_thread: bool, + + #[arg( + long, + help = t!("core_clap.disable_ipv6").to_string(), + default_value = "false" + )] + disable_ipv6: bool, + + #[arg( + long, + help = t!("core_clap.dev_name").to_string() + )] + dev_name: Option, + + #[arg( + long, + help = t!("core_clap.mtu").to_string() + )] + mtu: Option, + + #[arg( + long, + help = t!("core_clap.latency_first").to_string(), + default_value = "false" + )] + latency_first: bool, + + #[arg( + long, + help = t!("core_clap.exit_nodes").to_string(), + num_args = 0.. + )] + exit_nodes: Vec, + + #[arg( + long, + help = t!("core_clap.enable_exit_node").to_string(), + default_value = "false" + )] + enable_exit_node: bool, + + #[arg( + long, + help = t!("core_clap.proxy_forward_by_system").to_string(), + default_value = "false" + )] + proxy_forward_by_system: bool, + + #[arg( + long, + help = t!("core_clap.no_tun").to_string(), + default_value = "false" + )] + no_tun: bool, + + #[arg( + long, + help = t!("core_clap.use_smoltcp").to_string(), + default_value = "false" + )] + use_smoltcp: bool, + + #[arg( + long, + help = t!("core_clap.manual_routes").to_string(), + num_args = 0.. + )] + manual_routes: Option>, + + // if not in relay_network_whitelist: + // for foreign virtual network, will refuse the incoming connection + // for local virtual network, will refuse relaying tun packet + #[arg( + long, + help = t!("core_clap.relay_network_whitelist").to_string(), + num_args = 0.. + )] + relay_network_whitelist: Option>, + + #[arg( + long, + help = t!("core_clap.disable_p2p").to_string(), + default_value = "false" + )] + disable_p2p: bool, + + #[arg( + long, + help = t!("core_clap.disable_udp_hole_punching").to_string(), + default_value = "false" + )] + disable_udp_hole_punching: bool, + + #[arg( + long, + help = t!("core_clap.relay_all_peer_rpc").to_string(), + default_value = "false" + )] + relay_all_peer_rpc: bool, + + #[cfg(feature = "socks5")] + #[arg( + long, + help = t!("core_clap.socks5").to_string() + )] + socks5: Option, + + #[arg( + long, + help = t!("core_clap.ipv6_listener").to_string() + )] + ipv6_listener: Option, + + #[arg( + long, + help = t!("core_clap.compression").to_string(), + default_value = "none", + )] + compression: String, + + #[arg( + long, + help = t!("core_clap.bind_device").to_string() + )] + bind_device: Option, + + #[arg( + long, + help = t!("core_clap.enable_kcp_proxy").to_string(), + default_value = "false" + )] + enable_kcp_proxy: bool, + + #[arg( + long, + help = t!("core_clap.disable_kcp_input").to_string(), + default_value = "false" + )] + disable_kcp_input: bool, +} + +rust_i18n::i18n!("locales", fallback = "en"); + +impl Cli { + fn parse_listeners(no_listener: bool, listeners: Vec) -> anyhow::Result> { + if no_listener || listeners.is_empty() { + return Ok(vec![]); + } + + let origin_listners = listeners; + let mut listeners: Vec = Vec::new(); + if origin_listners.len() == 1 { + if let Ok(port) = origin_listners[0].parse::() { + for (proto, offset) in PROTO_PORT_OFFSET { + listeners.push(format!("{}://0.0.0.0:{}", proto, port + *offset)); + } + return Ok(listeners); + } + } + + for l in &origin_listners { + let proto_port: Vec<&str> = l.split(':').collect(); + if proto_port.len() > 2 { + if let Ok(url) = l.parse::() { + listeners.push(url.to_string()); + } else { + panic!("failed to parse listener: {}", l); + } + } else { + let Some((proto, offset)) = PROTO_PORT_OFFSET + .iter() + .find(|(proto, _)| *proto == proto_port[0]) + else { + return Err(anyhow::anyhow!("unknown protocol: {}", proto_port[0])); + }; + + let port = if proto_port.len() == 2 { + proto_port[1].parse::().unwrap() + } else { + 11010 + offset + }; + + listeners.push(format!("{}://0.0.0.0:{}", proto, port)); + } + } + + Ok(listeners) + } + + fn check_tcp_available(port: u16) -> Option { + let s = format!("0.0.0.0:{}", port).parse::().unwrap(); + TcpSocket::new_v4().unwrap().bind(s).map(|_| s).ok() + } + + fn parse_rpc_portal(rpc_portal: String) -> anyhow::Result { + if let Ok(port) = rpc_portal.parse::() { + if port == 0 { + // check tcp 15888 first + for i in 15888..15900 { + if let Some(s) = Cli::check_tcp_available(i) { + return Ok(s); + } + } + return Ok("0.0.0.0:0".parse().unwrap()); + } + return Ok(format!("0.0.0.0:{}", port).parse().unwrap()); + } + + Ok(rpc_portal.parse()?) + } +} + +impl TryFrom<&Cli> for TomlConfigLoader { + type Error = anyhow::Error; + + fn try_from(cli: &Cli) -> Result { + if let Some(config_file) = &cli.config_file { + println!( + "NOTICE: loading config file: {:?}, will ignore all command line flags\n", + config_file + ); + return Ok(TomlConfigLoader::new(config_file) + .with_context(|| format!("failed to load config file: {:?}", cli.config_file))?); + } + + let cfg = TomlConfigLoader::default(); + + cfg.set_hostname(cli.hostname.clone()); + + cfg.set_network_identity(NetworkIdentity::new( + cli.network_name.clone(), + cli.network_secret.clone(), + )); + + cfg.set_dhcp(cli.dhcp); + + if let Some(ipv4) = &cli.ipv4 { + cfg.set_ipv4(Some(ipv4.parse().with_context(|| { + format!("failed to parse ipv4 address: {}", ipv4) + })?)) + } + + let mut peers = Vec::::with_capacity(cli.peers.len()); + for p in &cli.peers { + peers.push(PeerConfig { + uri: p + .parse() + .with_context(|| format!("failed to parse peer uri: {}", p))?, + }); + } + cfg.set_peers(peers); + + cfg.set_listeners( + Cli::parse_listeners(cli.no_listener, cli.listeners.clone())? + .into_iter() + .map(|s| s.parse().unwrap()) + .collect(), + ); + + cfg.set_mapped_listeners(Some( + cli.mapped_listeners + .iter() + .map(|s| { + s.parse() + .with_context(|| format!("mapped listener is not a valid url: {}", s)) + .unwrap() + }) + .map(|s: url::Url| { + if s.port().is_none() { + panic!("mapped listener port is missing: {}", s); + } + s + }) + .collect(), + )); + + for n in cli.proxy_networks.iter() { + cfg.add_proxy_cidr( + n.parse() + .with_context(|| format!("failed to parse proxy network: {}", n))?, + ); + } + + cfg.set_rpc_portal( + Cli::parse_rpc_portal(cli.rpc_portal.clone()) + .with_context(|| format!("failed to parse rpc portal: {}", cli.rpc_portal))?, + ); + + if let Some(external_nodes) = cli.external_node.as_ref() { + let mut old_peers = cfg.get_peers(); + old_peers.push(PeerConfig { + uri: external_nodes.parse().with_context(|| { + format!("failed to parse external node uri: {}", external_nodes) + })?, + }); + cfg.set_peers(old_peers); + } + + if cli.console_log_level.is_some() { + cfg.set_console_logger_config(ConsoleLoggerConfig { + level: cli.console_log_level.clone(), + }); + } + + if cli.file_log_dir.is_some() || cli.file_log_level.is_some() { + cfg.set_file_logger_config(FileLoggerConfig { + level: cli.file_log_level.clone(), + dir: cli.file_log_dir.clone(), + file: Some(format!("easytier-{}", cli.instance_name)), + }); + } + + cfg.set_inst_name(cli.instance_name.clone()); + + if let Some(vpn_portal) = cli.vpn_portal.as_ref() { + let url: url::Url = vpn_portal + .parse() + .with_context(|| format!("failed to parse vpn portal url: {}", vpn_portal))?; + let host = url + .host_str() + .ok_or_else(|| anyhow::anyhow!("vpn portal url missing host"))?; + let port = url + .port() + .ok_or_else(|| anyhow::anyhow!("vpn portal url missing port"))?; + let client_cidr = url.path()[1..].parse().with_context(|| { + format!("failed to parse vpn portal client cidr: {}", url.path()) + })?; + let wireguard_listen: SocketAddr = format!("{}:{}", host, port).parse().unwrap(); + cfg.set_vpn_portal_config(VpnPortalConfig { + wireguard_listen, + client_cidr, + }); + } + + if let Some(manual_routes) = cli.manual_routes.as_ref() { + let mut routes = Vec::::with_capacity(manual_routes.len()); + for r in manual_routes { + routes.push( + r.parse() + .with_context(|| format!("failed to parse route: {}", r))?, + ); + } + cfg.set_routes(Some(routes)); + } + + #[cfg(feature = "socks5")] + if let Some(socks5_proxy) = cli.socks5 { + cfg.set_socks5_portal(Some( + format!("socks5://0.0.0.0:{}", socks5_proxy) + .parse() + .unwrap(), + )); + } + + let mut f = cfg.get_flags(); + if cli.default_protocol.is_some() { + f.default_protocol = cli.default_protocol.as_ref().unwrap().clone(); + } + f.enable_encryption = !cli.disable_encryption; + f.enable_ipv6 = !cli.disable_ipv6; + f.latency_first = cli.latency_first; + f.dev_name = cli.dev_name.clone().unwrap_or_default(); + if let Some(mtu) = cli.mtu { + f.mtu = mtu as u32; + } + f.enable_exit_node = cli.enable_exit_node; + f.proxy_forward_by_system = cli.proxy_forward_by_system; + f.no_tun = cli.no_tun || cfg!(not(feature = "tun")); + f.use_smoltcp = cli.use_smoltcp; + if let Some(wl) = cli.relay_network_whitelist.as_ref() { + f.relay_network_whitelist = wl.join(" "); + } + f.disable_p2p = cli.disable_p2p; + f.disable_udp_hole_punching = cli.disable_udp_hole_punching; + f.relay_all_peer_rpc = cli.relay_all_peer_rpc; + if let Some(ipv6_listener) = cli.ipv6_listener.as_ref() { + f.ipv6_listener = ipv6_listener + .parse() + .with_context(|| format!("failed to parse ipv6 listener: {}", ipv6_listener))? + } + f.multi_thread = cli.multi_thread; + f.data_compress_algo = match cli.compression.as_str() { + "none" => CompressionAlgoPb::None, + "zstd" => CompressionAlgoPb::Zstd, + _ => panic!( + "unknown compression algorithm: {}, supported: none, zstd", + cli.compression + ), + } + .into(); + if let Some(bind_device) = cli.bind_device { + f.bind_device = bind_device; + } + f.enable_kcp_proxy = cli.enable_kcp_proxy; + f.disable_kcp_input = cli.disable_kcp_input; + cfg.set_flags(f); + + cfg.set_exit_nodes(cli.exit_nodes.clone()); + + Ok(cfg) + } +} + +fn print_event(msg: String) { + println!( + "{}: {}", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), + msg + ); +} + +fn peer_conn_info_to_string(p: proto::cli::PeerConnInfo) -> String { + format!( + "my_peer_id: {}, dst_peer_id: {}, tunnel_info: {:?}", + p.my_peer_id, p.peer_id, p.tunnel + ) +} + +#[tracing::instrument] +pub fn handle_event(mut events: EventBusSubscriber) -> tokio::task::JoinHandle<()> { + tokio::spawn(async move { + while let Ok(e) = events.recv().await { + match e { + GlobalCtxEvent::PeerAdded(p) => { + print_event(format!("new peer added. peer_id: {}", p)); + } + + GlobalCtxEvent::PeerRemoved(p) => { + print_event(format!("peer removed. peer_id: {}", p)); + } + + GlobalCtxEvent::PeerConnAdded(p) => { + print_event(format!( + "new peer connection added. conn_info: {}", + peer_conn_info_to_string(p) + )); + } + + GlobalCtxEvent::PeerConnRemoved(p) => { + print_event(format!( + "peer connection removed. conn_info: {}", + peer_conn_info_to_string(p) + )); + } + + GlobalCtxEvent::ListenerAddFailed(p, msg) => { + print_event(format!( + "listener add failed. listener: {}, msg: {}", + p, msg + )); + } + + GlobalCtxEvent::ListenerAcceptFailed(p, msg) => { + print_event(format!( + "listener accept failed. listener: {}, msg: {}", + p, msg + )); + } + + GlobalCtxEvent::ListenerAdded(p) => { + if p.scheme() == "ring" { + continue; + } + print_event(format!("new listener added. listener: {}", p)); + } + + GlobalCtxEvent::ConnectionAccepted(local, remote) => { + print_event(format!( + "new connection accepted. local: {}, remote: {}", + local, remote + )); + } + + GlobalCtxEvent::ConnectionError(local, remote, err) => { + print_event(format!( + "connection error. local: {}, remote: {}, err: {}", + local, remote, err + )); + } + + GlobalCtxEvent::TunDeviceReady(dev) => { + print_event(format!("tun device ready. dev: {}", dev)); + } + + GlobalCtxEvent::TunDeviceError(err) => { + print_event(format!("tun device error. err: {}", err)); + } + + GlobalCtxEvent::Connecting(dst) => { + print_event(format!("connecting to peer. dst: {}", dst)); + } + + GlobalCtxEvent::ConnectError(dst, ip_version, err) => { + print_event(format!( + "connect to peer error. dst: {}, ip_version: {}, err: {}", + dst, ip_version, err + )); + } + + GlobalCtxEvent::VpnPortalClientConnected(portal, client_addr) => { + print_event(format!( + "vpn portal client connected. portal: {}, client_addr: {}", + portal, client_addr + )); + } + + GlobalCtxEvent::VpnPortalClientDisconnected(portal, client_addr) => { + print_event(format!( + "vpn portal client disconnected. portal: {}, client_addr: {}", + portal, client_addr + )); + } + + GlobalCtxEvent::DhcpIpv4Changed(old, new) => { + print_event(format!("dhcp ip changed. old: {:?}, new: {:?}", old, new)); + } + + GlobalCtxEvent::DhcpIpv4Conflicted(ip) => { + print_event(format!("dhcp ip conflict. ip: {:?}", ip)); + } + } + } + }) +} + +#[cfg(target_os = "windows")] +fn win_service_set_work_dir(service_name: &std::ffi::OsString) -> anyhow::Result<()> { + use easytier::common::constants::WIN_SERVICE_WORK_DIR_REG_KEY; + use winreg::enums::*; + use winreg::RegKey; + + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let key = hklm.open_subkey_with_flags(WIN_SERVICE_WORK_DIR_REG_KEY, KEY_READ)?; + let dir_pat_str = key.get_value::(service_name)?; + let dir_path = std::fs::canonicalize(dir_pat_str)?; + + std::env::set_current_dir(dir_path)?; + + Ok(()) +} + +#[cfg(target_os = "windows")] +fn win_service_event_loop( + stop_notify: std::sync::Arc, + cli: Cli, + status_handle: windows_service::service_control_handler::ServiceStatusHandle, +) { + use std::time::Duration; + use tokio::runtime::Runtime; + use windows_service::service::*; + + let normal_status = ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }; + let error_status = ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::ServiceSpecific(1u32), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }; + + std::thread::spawn(move || { + let rt = Runtime::new().unwrap(); + rt.block_on(async move { + tokio::select! { + res = run_main(cli) => { + match res { + Ok(_) => { + status_handle.set_service_status(normal_status).unwrap(); + std::process::exit(0); + } + Err(e) => { + status_handle.set_service_status(error_status).unwrap(); + eprintln!("error: {}", e); + } + } + }, + _ = stop_notify.notified() => { + _ = status_handle.set_service_status(normal_status); + std::process::exit(0); + } + } + }); + }); +} + +#[cfg(target_os = "windows")] +fn win_service_main(arg: Vec) { + use std::sync::Arc; + use std::time::Duration; + use tokio::sync::Notify; + use windows_service::service::*; + use windows_service::service_control_handler::*; + + _ = win_service_set_work_dir(&arg[0]); + + let cli = Cli::parse(); + + let stop_notify_send = Arc::new(Notify::new()); + let stop_notify_recv = Arc::clone(&stop_notify_send); + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + ServiceControl::Stop => { + stop_notify_send.notify_one(); + ServiceControlHandlerResult::NoError + } + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + let status_handle = register(String::new(), event_handler).expect("register service fail"); + let next_status = ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }; + status_handle + .set_service_status(next_status) + .expect("set service status fail"); + + win_service_event_loop(stop_notify_recv, cli, status_handle); +} + +async fn run_main(cli: Cli) -> anyhow::Result<()> { + let cfg = TomlConfigLoader::try_from(&cli)?; + init_logger(&cfg, false)?; + + if cli.config_server.is_some() { + let config_server_url_s = cli.config_server.clone().unwrap(); + let config_server_url = match url::Url::parse(&config_server_url_s) { + Ok(u) => u, + Err(_) => format!( + "udp://config-server.easytier.cn:22020/{}", + config_server_url_s + ) + .parse() + .unwrap(), + }; + + let mut c_url = config_server_url.clone(); + c_url.set_path(""); + let token = config_server_url + .path_segments() + .and_then(|mut x| x.next()) + .map(|x| x.to_string()) + .unwrap_or_default(); + + println!( + "Entering config client mode...\n server: {}\n token: {}", + c_url, token, + ); + + println!("Official config website: https://easytier.cn/web"); + + if token.is_empty() { + panic!("empty token"); + } + + let config = TomlConfigLoader::default(); + let global_ctx = Arc::new(GlobalCtx::new(config)); + global_ctx.replace_stun_info_collector(Box::new(MockStunInfoCollector { + udp_nat_type: NatType::Unknown, + })); + let mut flags = global_ctx.get_flags(); + flags.bind_device = false; + global_ctx.set_flags(flags); + let _wc = web_client::WebClient::new( + create_connector_by_url(c_url.as_str(), &global_ctx).await?, + token.to_string(), + ); + tokio::signal::ctrl_c().await.unwrap(); + DNSTunnelConnector::new("".parse().unwrap(), global_ctx); + return Ok(()); + } + + println!("Starting easytier with config:"); + println!("############### TOML ###############\n"); + println!("{}", cfg.dump()); + println!("-----------------------------------"); + + let mut l = launcher::NetworkInstance::new(cfg).set_fetch_node_info(false); + let _t = ScopedTask::from(handle_event(l.start().unwrap())); + if let Some(e) = l.wait().await { + anyhow::bail!("launcher error: {}", e); + } + Ok(()) +} + +#[tokio::main(flavor = "current_thread")] +async fn main() { + let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); + rust_i18n::set_locale(&locale); + setup_panic_handler(); + + #[cfg(target_os = "windows")] + match windows_service::service_dispatcher::start(String::new(), ffi_service_main) { + Ok(_) => std::thread::park(), + Err(e) => { + let should_panic = if let windows_service::Error::Winapi(ref io_error) = e { + io_error.raw_os_error() != Some(0x427) // ERROR_FAILED_SERVICE_CONTROLLER_CONNECT + } else { + true + }; + + if should_panic { + panic!("SCM start an error: {}", e); + } + } + }; + + let cli = Cli::parse(); + + if let Err(e) = run_main(cli).await { + eprintln!("error: {:?}", e); + std::process::exit(1); + } +} diff --git a/rust/easytier/src/gateway/fast_socks5/LICENSE b/rust/easytier/src/gateway/fast_socks5/LICENSE new file mode 100644 index 0000000..943eda1 --- /dev/null +++ b/rust/easytier/src/gateway/fast_socks5/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Jonathan Dizdarevic + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/rust/easytier/src/gateway/fast_socks5/README.md b/rust/easytier/src/gateway/fast_socks5/README.md new file mode 100644 index 0000000..2f4f517 --- /dev/null +++ b/rust/easytier/src/gateway/fast_socks5/README.md @@ -0,0 +1 @@ +Code is modified from https://github.com/dizda/fast-socks5 diff --git a/rust/easytier/src/gateway/fast_socks5/mod.rs b/rust/easytier/src/gateway/fast_socks5/mod.rs new file mode 100644 index 0000000..133c94c --- /dev/null +++ b/rust/easytier/src/gateway/fast_socks5/mod.rs @@ -0,0 +1,314 @@ +//! Fast SOCKS5 client/server implementation written in Rust async/.await (with tokio). +//! +//! This library is maintained by [anyip.io](https://anyip.io/) a residential and mobile socks5 proxy provider. +//! +//! ## Features +//! +//! - An `async`/`.await` [SOCKS5](https://tools.ietf.org/html/rfc1928) implementation. +//! - An `async`/`.await` [SOCKS4 Client](https://www.openssh.com/txt/socks4.protocol) implementation. +//! - An `async`/`.await` [SOCKS4a Client](https://www.openssh.com/txt/socks4a.protocol) implementation. +//! - No **unsafe** code +//! - Built on-top of `tokio` library +//! - Ultra lightweight and scalable +//! - No system dependencies +//! - Cross-platform +//! - Authentication methods: +//! - No-Auth method +//! - Username/Password auth method +//! - Custom auth methods can be implemented via the Authentication Trait +//! - Credentials returned on authentication success +//! - All SOCKS5 RFC errors (replies) should be mapped +//! - `AsyncRead + AsyncWrite` traits are implemented on Socks5Stream & Socks5Socket +//! - `IPv4`, `IPv6`, and `Domains` types are supported +//! - Config helper for Socks5Server +//! - Helpers to run a Socks5Server à la *"std's TcpStream"* via `incoming.next().await` +//! - Examples come with real cases commands scenarios +//! - Can disable `DNS resolving` +//! - Can skip the authentication/handshake process, which will directly handle command's request (useful to save useless round-trips in a current authenticated environment) +//! - Can disable command execution (useful if you just want to forward the request to a different server) +//! +//! +//! ## Install +//! +//! Open in [crates.io](https://crates.io/crates/fast-socks5). +//! +//! +//! ## Examples +//! +//! Please check [`examples`](https://github.com/dizda/fast-socks5/tree/master/examples) directory. + +#![forbid(unsafe_code)] + +pub mod server; +pub mod util; + +use anyhow::Context; +use std::fmt; +use std::io; +use thiserror::Error; +use util::target_addr::read_address; +use util::target_addr::TargetAddr; +use util::target_addr::ToTargetAddr; + +use tokio::io::AsyncReadExt; + +use tracing::error; + +use crate::read_exact; + +#[rustfmt::skip] +pub mod consts { + pub const SOCKS5_VERSION: u8 = 0x05; + + pub const SOCKS5_AUTH_METHOD_NONE: u8 = 0x00; + pub const SOCKS5_AUTH_METHOD_GSSAPI: u8 = 0x01; + pub const SOCKS5_AUTH_METHOD_PASSWORD: u8 = 0x02; + pub const SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE: u8 = 0xff; + + pub const SOCKS5_CMD_TCP_CONNECT: u8 = 0x01; + pub const SOCKS5_CMD_TCP_BIND: u8 = 0x02; + pub const SOCKS5_CMD_UDP_ASSOCIATE: u8 = 0x03; + + pub const SOCKS5_ADDR_TYPE_IPV4: u8 = 0x01; + pub const SOCKS5_ADDR_TYPE_DOMAIN_NAME: u8 = 0x03; + pub const SOCKS5_ADDR_TYPE_IPV6: u8 = 0x04; + + pub const SOCKS5_REPLY_SUCCEEDED: u8 = 0x00; + pub const SOCKS5_REPLY_GENERAL_FAILURE: u8 = 0x01; + pub const SOCKS5_REPLY_CONNECTION_NOT_ALLOWED: u8 = 0x02; + pub const SOCKS5_REPLY_NETWORK_UNREACHABLE: u8 = 0x03; + pub const SOCKS5_REPLY_HOST_UNREACHABLE: u8 = 0x04; + pub const SOCKS5_REPLY_CONNECTION_REFUSED: u8 = 0x05; + pub const SOCKS5_REPLY_TTL_EXPIRED: u8 = 0x06; + pub const SOCKS5_REPLY_COMMAND_NOT_SUPPORTED: u8 = 0x07; + pub const SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED: u8 = 0x08; +} + +#[derive(Debug, PartialEq)] +pub enum Socks5Command { + TCPConnect, + TCPBind, + UDPAssociate, +} + +#[allow(dead_code)] +impl Socks5Command { + #[inline] + #[rustfmt::skip] + fn as_u8(&self) -> u8 { + match self { + Socks5Command::TCPConnect => consts::SOCKS5_CMD_TCP_CONNECT, + Socks5Command::TCPBind => consts::SOCKS5_CMD_TCP_BIND, + Socks5Command::UDPAssociate => consts::SOCKS5_CMD_UDP_ASSOCIATE, + } + } + + #[inline] + #[rustfmt::skip] + fn from_u8(code: u8) -> Option { + match code { + consts::SOCKS5_CMD_TCP_CONNECT => Some(Socks5Command::TCPConnect), + consts::SOCKS5_CMD_TCP_BIND => Some(Socks5Command::TCPBind), + consts::SOCKS5_CMD_UDP_ASSOCIATE => Some(Socks5Command::UDPAssociate), + _ => None, + } + } +} + +#[derive(Debug, PartialEq)] +pub enum AuthenticationMethod { + None, + Password { username: String, password: String }, +} + +impl AuthenticationMethod { + #[inline] + #[rustfmt::skip] + fn as_u8(&self) -> u8 { + match self { + AuthenticationMethod::None => consts::SOCKS5_AUTH_METHOD_NONE, + AuthenticationMethod::Password {..} => + consts::SOCKS5_AUTH_METHOD_PASSWORD + } + } + + #[inline] + #[rustfmt::skip] + fn from_u8(code: u8) -> Option { + match code { + consts::SOCKS5_AUTH_METHOD_NONE => Some(AuthenticationMethod::None), + consts::SOCKS5_AUTH_METHOD_PASSWORD => Some(AuthenticationMethod::Password { username: "test".to_string(), password: "test".to_string()}), + _ => None, + } + } +} + +impl fmt::Display for AuthenticationMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + AuthenticationMethod::None => f.write_str("AuthenticationMethod::None"), + AuthenticationMethod::Password { .. } => f.write_str("AuthenticationMethod::Password"), + } + } +} + +//impl Vec { +// pub fn as_bytes(&self) -> &[u8] { +// self.iter().map(|l| l.as_u8()).collect() +// } +//} +// +//impl From<&[AuthenticationMethod]> for &[u8] { +// fn from(_: Vec) -> Self { +// &[0x00] +// } +//} + +#[derive(Error, Debug)] +pub enum SocksError { + #[error("i/o error: {0}")] + Io(#[from] io::Error), + #[error("the data for key `{0}` is not available")] + Redaction(String), + #[error("invalid header (expected {expected:?}, found {found:?})")] + InvalidHeader { expected: String, found: String }, + + #[error("Auth method unacceptable `{0:?}`.")] + AuthMethodUnacceptable(Vec), + #[error("Unsupported SOCKS version `{0}`.")] + UnsupportedSocksVersion(u8), + #[error("Domain exceeded max sequence length")] + ExceededMaxDomainLen(usize), + #[error("Authentication failed `{0}`")] + AuthenticationFailed(String), + #[error("Authentication rejected `{0}`")] + AuthenticationRejected(String), + + #[error("Error with reply: {0}.")] + ReplyError(#[from] ReplyError), + + #[error("Argument input error: `{0}`.")] + ArgumentInputError(&'static str), + + // #[error("Other: `{0}`.")] + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +pub type Result = core::result::Result; + +/// SOCKS5 reply code +#[derive(Error, Debug, Copy, Clone)] +pub enum ReplyError { + #[error("Succeeded")] + Succeeded, + #[error("General failure")] + GeneralFailure, + #[error("Connection not allowed by ruleset")] + ConnectionNotAllowed, + #[error("Network unreachable")] + NetworkUnreachable, + #[error("Host unreachable")] + HostUnreachable, + #[error("Connection refused")] + ConnectionRefused, + #[error("Connection timeout")] + ConnectionTimeout, + #[error("TTL expired")] + TtlExpired, + #[error("Command not supported")] + CommandNotSupported, + #[error("Address type not supported")] + AddressTypeNotSupported, + // OtherReply(u8), +} + +impl ReplyError { + #[inline] + #[rustfmt::skip] + pub fn as_u8(self) -> u8 { + match self { + ReplyError::Succeeded => consts::SOCKS5_REPLY_SUCCEEDED, + ReplyError::GeneralFailure => consts::SOCKS5_REPLY_GENERAL_FAILURE, + ReplyError::ConnectionNotAllowed => consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED, + ReplyError::NetworkUnreachable => consts::SOCKS5_REPLY_NETWORK_UNREACHABLE, + ReplyError::HostUnreachable => consts::SOCKS5_REPLY_HOST_UNREACHABLE, + ReplyError::ConnectionRefused => consts::SOCKS5_REPLY_CONNECTION_REFUSED, + ReplyError::ConnectionTimeout => consts::SOCKS5_REPLY_TTL_EXPIRED, + ReplyError::TtlExpired => consts::SOCKS5_REPLY_TTL_EXPIRED, + ReplyError::CommandNotSupported => consts::SOCKS5_REPLY_COMMAND_NOT_SUPPORTED, + ReplyError::AddressTypeNotSupported => consts::SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED, +// ReplyError::OtherReply(c) => c, + } + } + + #[inline] + #[rustfmt::skip] + pub fn from_u8(code: u8) -> ReplyError { + match code { + consts::SOCKS5_REPLY_SUCCEEDED => ReplyError::Succeeded, + consts::SOCKS5_REPLY_GENERAL_FAILURE => ReplyError::GeneralFailure, + consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED => ReplyError::ConnectionNotAllowed, + consts::SOCKS5_REPLY_NETWORK_UNREACHABLE => ReplyError::NetworkUnreachable, + consts::SOCKS5_REPLY_HOST_UNREACHABLE => ReplyError::HostUnreachable, + consts::SOCKS5_REPLY_CONNECTION_REFUSED => ReplyError::ConnectionRefused, + consts::SOCKS5_REPLY_TTL_EXPIRED => ReplyError::TtlExpired, + consts::SOCKS5_REPLY_COMMAND_NOT_SUPPORTED => ReplyError::CommandNotSupported, + consts::SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED => ReplyError::AddressTypeNotSupported, +// _ => ReplyError::OtherReply(code), + _ => unreachable!("ReplyError code unsupported."), + } + } +} + +/// Generate UDP header +/// +/// # UDP Request header structure. +/// ```text +/// +----+------+------+----------+----------+----------+ +/// |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | +/// +----+------+------+----------+----------+----------+ +/// | 2 | 1 | 1 | Variable | 2 | Variable | +/// +----+------+------+----------+----------+----------+ +/// +/// The fields in the UDP request header are: +/// +/// o RSV Reserved X'0000' +/// o FRAG Current fragment number +/// o ATYP address type of following addresses: +/// o IP V4 address: X'01' +/// o DOMAINNAME: X'03' +/// o IP V6 address: X'04' +/// o DST.ADDR desired destination address +/// o DST.PORT desired destination port +/// o DATA user data +/// ``` +pub fn new_udp_header(target_addr: T) -> Result> { + let mut header = vec![ + 0, 0, // RSV + 0, // FRAG + ]; + header.append(&mut target_addr.to_target_addr()?.to_be_bytes()?); + + Ok(header) +} + +/// Parse data from UDP client on raw buffer, return (frag, target_addr, payload). +pub async fn parse_udp_request<'a>(mut req: &'a [u8]) -> Result<(u8, TargetAddr, &'a [u8])> { + let rsv = read_exact!(req, [0u8; 2]).context("Malformed request")?; + + if !rsv.eq(&[0u8; 2]) { + return Err(ReplyError::GeneralFailure.into()); + } + + let [frag, atyp] = read_exact!(req, [0u8; 2]).context("Malformed request")?; + + let target_addr = read_address(&mut req, atyp).await.map_err(|e| { + // print explicit error + error!("{:#}", e); + // then convert it to a reply + ReplyError::AddressTypeNotSupported + })?; + + Ok((frag, target_addr, req)) +} diff --git a/rust/easytier/src/gateway/fast_socks5/server.rs b/rust/easytier/src/gateway/fast_socks5/server.rs new file mode 100644 index 0000000..44b7282 --- /dev/null +++ b/rust/easytier/src/gateway/fast_socks5/server.rs @@ -0,0 +1,842 @@ +use super::new_udp_header; +use super::parse_udp_request; +use super::read_exact; +use super::util::stream::tcp_connect_with_timeout; +use super::util::target_addr::{read_address, TargetAddr}; +use super::Socks5Command; +use super::{consts, AuthenticationMethod, ReplyError, Result, SocksError}; +use anyhow::Context; +use std::io; +use std::net::IpAddr; +use std::net::Ipv4Addr; +use std::net::{SocketAddr, ToSocketAddrs as StdToSocketAddrs}; +use std::ops::Deref; +use std::pin::Pin; +use std::sync::Arc; +use std::task::Poll; +use tokio::io::AsyncReadExt; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::net::UdpSocket; +use tokio::try_join; + +use tracing::{debug, error, info, trace}; + +#[derive(Clone)] +pub struct Config { + /// Timeout of the command request + request_timeout: u64, + /// Avoid useless roundtrips if we don't need the Authentication layer + skip_auth: bool, + /// Enable dns-resolving + dns_resolve: bool, + /// Enable command execution + execute_command: bool, + /// Enable UDP support + allow_udp: bool, + /// For some complex scenarios, we may want to either accept Username/Password configuration + /// or IP Whitelisting, in case the client send only 1-2 auth methods (no auth) rather than 3 (with auth) + allow_no_auth: bool, + /// Contains the authentication trait to use the user against with + auth: Option>, +} + +impl Default for Config { + fn default() -> Self { + Config { + request_timeout: 10, + skip_auth: false, + dns_resolve: true, + execute_command: true, + allow_udp: false, + allow_no_auth: false, + auth: None, + } + } +} + +/// Use this trait to handle a custom authentication on your end. +#[async_trait::async_trait] +pub trait Authentication: Send + Sync { + type Item; + + async fn authenticate(&self, credentials: Option<(String, String)>) -> Option; +} + +/// Basic user/pass auth method provided. +pub struct SimpleUserPassword { + pub username: String, + pub password: String, +} + +/// The struct returned when the user has successfully authenticated +pub struct AuthSucceeded { + pub username: String, +} + +/// This is an example to auth via simple credentials. +/// If the auth succeed, we return the username authenticated with, for further uses. +#[async_trait::async_trait] +impl Authentication for SimpleUserPassword { + type Item = AuthSucceeded; + + async fn authenticate(&self, credentials: Option<(String, String)>) -> Option { + if let Some((username, password)) = credentials { + // Client has supplied credentials + if username == self.username && password == self.password { + // Some() will allow the authentication and the credentials + // will be forwarded to the socket + Some(AuthSucceeded { username }) + } else { + // Credentials incorrect, we deny the auth + None + } + } else { + // The client hasn't supplied any credentials, which only happens + // when `Config::allow_no_auth()` is set as `true` + None + } + } +} + +/// This will simply return Option::None, which denies the authentication +#[derive(Copy, Clone, Default)] +pub struct DenyAuthentication {} + +#[async_trait::async_trait] +impl Authentication for DenyAuthentication { + type Item = (); + + async fn authenticate(&self, _credentials: Option<(String, String)>) -> Option { + None + } +} + +/// While this one will always allow the user in. +#[derive(Copy, Clone, Default)] +pub struct AcceptAuthentication {} + +#[async_trait::async_trait] +impl Authentication for AcceptAuthentication { + type Item = (); + + async fn authenticate(&self, _credentials: Option<(String, String)>) -> Option { + Some(()) + } +} + +impl Config { + /// How much time it should wait until the request timeout. + pub fn set_request_timeout(&mut self, n: u64) -> &mut Self { + self.request_timeout = n; + self + } + + /// Skip the entire auth/handshake part, which means the server will directly wait for + /// the command request. + pub fn set_skip_auth(&mut self, value: bool) -> &mut Self { + self.skip_auth = value; + self.auth = None; + self + } + + /// Enable authentication + /// 'static lifetime for Authentication avoid us to use `dyn Authentication` + /// and set the Arc before calling the function. + pub fn with_authentication(self, authentication: T) -> Config { + Config { + request_timeout: self.request_timeout, + skip_auth: self.skip_auth, + dns_resolve: self.dns_resolve, + execute_command: self.execute_command, + allow_udp: self.allow_udp, + allow_no_auth: self.allow_no_auth, + auth: Some(Arc::new(authentication)), + } + } + + /// For some complex scenarios, we may want to either accept Username/Password configuration + /// or IP Whitelisting, in case the client send only 2 auth methods rather than 3 (with auth) + pub fn set_allow_no_auth(&mut self, value: bool) -> &mut Self { + self.allow_no_auth = value; + self + } + + /// Set whether or not to execute commands + pub fn set_execute_command(&mut self, value: bool) -> &mut Self { + self.execute_command = value; + self + } + + /// Will the server perform dns resolve + pub fn set_dns_resolve(&mut self, value: bool) -> &mut Self { + self.dns_resolve = value; + self + } + + /// Set whether or not to allow udp traffic + pub fn set_udp_support(&mut self, value: bool) -> &mut Self { + self.allow_udp = value; + self + } +} + +#[async_trait::async_trait] +pub trait AsyncTcpConnector { + type S: AsyncRead + AsyncWrite + Unpin + Send + Sync; + + async fn tcp_connect(&self, addr: SocketAddr, timeout_s: u64) -> Result; +} + +pub struct DefaultTcpConnector {} + +#[async_trait::async_trait] +impl AsyncTcpConnector for DefaultTcpConnector { + type S = TcpStream; + + async fn tcp_connect(&self, addr: SocketAddr, timeout_s: u64) -> Result { + tcp_connect_with_timeout(addr, timeout_s).await + } +} + +/// Wrap TcpStream and contains Socks5 protocol implementation. +pub struct Socks5Socket +{ + inner: T, + config: Arc>, + auth: AuthenticationMethod, + target_addr: Option, + cmd: Option, + /// Socket address which will be used in the reply message. + reply_ip: Option, + /// If the client has been authenticated, that's where we store his credentials + /// to be accessed from the socket + credentials: Option, + tcp_connector: C, +} + +impl + Socks5Socket +{ + pub fn new(socket: T, config: Arc>, tcp_connector: C) -> Self { + Socks5Socket { + inner: socket, + config, + auth: AuthenticationMethod::None, + target_addr: None, + cmd: None, + reply_ip: None, + credentials: None, + tcp_connector, + } + } + + /// Set the bind IP address in Socks5Reply. + /// + /// Only the inner socket owner knows the correct reply bind addr, so leave this field to be + /// populated. For those strict clients, users can use this function to set the correct IP + /// address. + /// + /// Most popular SOCKS5 clients [1] [2] ignore BND.ADDR and BND.PORT the reply of command + /// CONNECT, but this field could be useful in some other command, such as UDP ASSOCIATE. + /// + /// [1]: https://github.com/chromium/chromium/blob/bd2c7a8b65ec42d806277dd30f138a673dec233a/net/socket/socks5_client_socket.cc#L481 + /// [2]: https://github.com/curl/curl/blob/d15692ebbad5e9cfb871b0f7f51a73e43762cee2/lib/socks.c#L978 + pub fn set_reply_ip(&mut self, addr: IpAddr) { + self.reply_ip = Some(addr); + } + + /// Process clients SOCKS requests + /// This is the entry point where a whole request is processed. + pub async fn upgrade_to_socks5(mut self) -> Result> { + trace!("upgrading to socks5..."); + + // Handshake + if !self.config.skip_auth { + let methods = self.get_methods().await?; + + let auth_method = self.can_accept_method(methods).await?; + + if self.config.auth.is_some() { + let credentials = self.authenticate(auth_method).await?; + self.credentials = Some(credentials); + } + } else { + debug!("skipping auth"); + } + + match self.request().await { + Ok(_) => {} + Err(SocksError::ReplyError(e)) => { + // If a reply error has been returned, we send it to the client + self.reply_error(&e).await?; + return Err(e.into()); // propagate the error to end this connection's task + } + // if any other errors has been detected, we simply end connection's task + Err(d) => return Err(d), + }; + + Ok(self) + } + + /// Consumes the `Socks5Socket`, returning the wrapped stream. + pub fn into_inner(self) -> T { + self.inner + } + + /// Read the authentication method provided by the client. + /// A client send a list of methods that he supports, he could send + /// + /// - 0: Non auth + /// - 2: Auth with username/password + /// + /// Altogether, then the server choose to use of of these, + /// or deny the handshake (thus the connection). + /// + /// # Examples + /// ```text + /// {SOCKS Version, methods-length} + /// eg. (non-auth) {5, 2} + /// eg. (auth) {5, 3} + /// ``` + /// + async fn get_methods(&mut self) -> Result> { + trace!("Socks5Socket: get_methods()"); + // read the first 2 bytes which contains the SOCKS version and the methods len() + let [version, methods_len] = + read_exact!(self.inner, [0u8; 2]).context("Can't read methods")?; + debug!( + "Handshake headers: [version: {version}, methods len: {len}]", + version = version, + len = methods_len, + ); + + if version != consts::SOCKS5_VERSION { + return Err(SocksError::UnsupportedSocksVersion(version)); + } + + // {METHODS available from the client} + // eg. (non-auth) {0, 1} + // eg. (auth) {0, 1, 2} + let methods = read_exact!(self.inner, vec![0u8; methods_len as usize]) + .context("Can't get methods.")?; + debug!("methods supported sent by the client: {:?}", &methods); + + // Return methods available + Ok(methods) + } + + /// Decide to whether or not, accept the authentication method. + /// Don't forget that the methods list sent by the client, contains one or more methods. + /// + /// # Request + /// + /// Client send an array of 3 entries: [0, 1, 2] + /// ```text + /// {SOCKS Version, Authentication chosen} + /// eg. (non-auth) {5, 0} + /// eg. (GSSAPI) {5, 1} + /// eg. (auth) {5, 2} + /// ``` + /// + /// # Response + /// ```text + /// eg. (accept non-auth) {5, 0x00} + /// eg. (non-acceptable) {5, 0xff} + /// ``` + /// + async fn can_accept_method(&mut self, client_methods: Vec) -> Result { + let method_supported; + + if let Some(_auth) = self.config.auth.as_ref() { + if client_methods.contains(&consts::SOCKS5_AUTH_METHOD_PASSWORD) { + // can auth with password + method_supported = consts::SOCKS5_AUTH_METHOD_PASSWORD; + } else { + // client hasn't provided a password + if self.config.allow_no_auth { + // but we allow no auth, for ip whitelisting + method_supported = consts::SOCKS5_AUTH_METHOD_NONE; + } else { + // we don't allow no auth, so we deny the entry + debug!("Don't support this auth method, reply with (0xff)"); + self.inner + .write_all(&[ + consts::SOCKS5_VERSION, + consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE, + ]) + .await + .context("Can't reply with method not acceptable.")?; + + return Err(SocksError::AuthMethodUnacceptable(client_methods)); + } + } + } else { + method_supported = consts::SOCKS5_AUTH_METHOD_NONE; + } + + debug!( + "Reply with method {} ({})", + AuthenticationMethod::from_u8(method_supported).context("Method not supported")?, + method_supported + ); + self.inner + .write(&[consts::SOCKS5_VERSION, method_supported]) + .await + .context("Can't reply with method auth-none")?; + Ok(method_supported) + } + + async fn read_username_password(socket: &mut T) -> Result<(String, String)> { + trace!("Socks5Socket: authenticate()"); + let [version, user_len] = read_exact!(socket, [0u8; 2]).context("Can't read user len")?; + debug!( + "Auth: [version: {version}, user len: {len}]", + version = version, + len = user_len, + ); + + if user_len < 1 { + return Err(SocksError::AuthenticationFailed(format!( + "Username malformed ({} chars)", + user_len + ))); + } + + let username = + read_exact!(socket, vec![0u8; user_len as usize]).context("Can't get username.")?; + debug!("username bytes: {:?}", &username); + + let [pass_len] = read_exact!(socket, [0u8; 1]).context("Can't read pass len")?; + debug!("Auth: [pass len: {len}]", len = pass_len,); + + if pass_len < 1 { + return Err(SocksError::AuthenticationFailed(format!( + "Password malformed ({} chars)", + pass_len + ))); + } + + let password = + read_exact!(socket, vec![0u8; pass_len as usize]).context("Can't get password.")?; + debug!("password bytes: {:?}", &password); + + let username = String::from_utf8(username).context("Failed to convert username")?; + let password = String::from_utf8(password).context("Failed to convert password")?; + + Ok((username, password)) + } + + /// Only called if + /// - this server has `Authentication` trait implemented. + /// - and the client supports authentication via username/password + /// - or the client doesn't send authentication, but we let the trait decides if the `allow_no_auth()` set as `true` + async fn authenticate(&mut self, auth_method: u8) -> Result { + let credentials = if auth_method == consts::SOCKS5_AUTH_METHOD_PASSWORD { + let credentials = Self::read_username_password(&mut self.inner).await?; + Some(credentials) + } else { + // the client hasn't provided any credentials, the function auth.authenticate() + // will then check None, according to other parameters provided by the trait + // such as IP, etc. + None + }; + + let auth = self.config.auth.as_ref().context("No auth module")?; + + if let Some(credentials) = auth.authenticate(credentials).await { + if auth_method == consts::SOCKS5_AUTH_METHOD_PASSWORD { + // only the password way expect to write a response at this moment + self.inner + .write_all(&[1, consts::SOCKS5_REPLY_SUCCEEDED]) + .await + .context("Can't reply auth success")?; + } + + info!("User logged successfully."); + + return Ok(credentials); + } else { + self.inner + .write_all(&[1, consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE]) + .await + .context("Can't reply with auth method not acceptable.")?; + + return Err(SocksError::AuthenticationRejected(format!( + "Authentication, rejected." + ))); + } + } + + /// Wrapper to principally cover ReplyError types for both functions read & execute request. + async fn request(&mut self) -> Result<()> { + self.read_command().await?; + + if self.config.dns_resolve { + self.resolve_dns().await?; + } else { + debug!("Domain won't be resolved because `dns_resolve`'s config has been turned off.") + } + + if self.config.execute_command { + self.execute_command().await?; + } + + Ok(()) + } + + /// Reply error to the client with the reply code according to the RFC. + async fn reply_error(&mut self, error: &ReplyError) -> Result<()> { + let reply = new_reply(error, "0.0.0.0:0".parse().unwrap()); + debug!("reply error to be written: {:?}", &reply); + + self.inner + .write(&reply) + .await + .context("Can't write the reply!")?; + + self.inner.flush().await.context("Can't flush the reply!")?; + + Ok(()) + } + + /// Decide to whether or not, accept the authentication method. + /// Don't forget that the methods list sent by the client, contains one or more methods. + /// + /// # Request + /// ```text + /// +----+-----+-------+------+----------+----------+ + /// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + /// +----+-----+-------+------+----------+----------+ + /// | 1 | 1 | 1 | 1 | Variable | 2 | + /// +----+-----+-------+------+----------+----------+ + /// ``` + /// + /// It the request is correct, it should returns a ['SocketAddr']. + /// + async fn read_command(&mut self) -> Result<()> { + let [version, cmd, rsv, address_type] = + read_exact!(self.inner, [0u8; 4]).context("Malformed request")?; + debug!( + "Request: [version: {version}, command: {cmd}, rev: {rsv}, address_type: {address_type}]", + version = version, + cmd = cmd, + rsv = rsv, + address_type = address_type, + ); + + if version != consts::SOCKS5_VERSION { + return Err(SocksError::UnsupportedSocksVersion(version)); + } + + match Socks5Command::from_u8(cmd) { + None => return Err(ReplyError::CommandNotSupported.into()), + Some(cmd) => match cmd { + Socks5Command::TCPConnect => { + self.cmd = Some(cmd); + } + Socks5Command::UDPAssociate => { + if !self.config.allow_udp { + return Err(ReplyError::CommandNotSupported.into()); + } + self.cmd = Some(cmd); + } + Socks5Command::TCPBind => return Err(ReplyError::CommandNotSupported.into()), + }, + } + + // Guess address type + let target_addr = read_address(&mut self.inner, address_type) + .await + .map_err(|e| { + // print explicit error + error!("{:#}", e); + // then convert it to a reply + ReplyError::AddressTypeNotSupported + })?; + + self.target_addr = Some(target_addr); + + debug!("Request target is {}", self.target_addr.as_ref().unwrap()); + + Ok(()) + } + + /// This function is public, it can be call manually on your own-willing + /// if config flag has been turned off: `Config::dns_resolve == false`. + pub async fn resolve_dns(&mut self) -> Result<()> { + trace!("resolving dns"); + if let Some(target_addr) = self.target_addr.take() { + // decide whether we have to resolve DNS or not + self.target_addr = match target_addr { + TargetAddr::Domain(_, _) => Some(target_addr.resolve_dns().await?), + TargetAddr::Ip(_) => Some(target_addr), + }; + } + + Ok(()) + } + + /// Execute the socks5 command that the client wants. + async fn execute_command(&mut self) -> Result<()> { + match &self.cmd { + None => Err(ReplyError::CommandNotSupported.into()), + Some(cmd) => match cmd { + Socks5Command::TCPBind => Err(ReplyError::CommandNotSupported.into()), + Socks5Command::TCPConnect => return self.execute_command_connect().await, + Socks5Command::UDPAssociate => { + if self.config.allow_udp { + return self.execute_command_udp_assoc().await; + } else { + Err(ReplyError::CommandNotSupported.into()) + } + } + }, + } + } + + /// Connect to the target address that the client wants, + /// then forward the data between them (client <=> target address). + async fn execute_command_connect(&mut self) -> Result<()> { + // async-std's ToSocketAddrs doesn't supports external trait implementation + // @see https://github.com/async-rs/async-std/issues/539 + let addr = self + .target_addr + .as_ref() + .context("target_addr empty")? + .to_socket_addrs()? + .next() + .context("unreachable")?; + + // TCP connect with timeout, to avoid memory leak for connection that takes forever + let outbound = self + .tcp_connector + .tcp_connect(addr, self.config.request_timeout) + .await?; + + debug!("Connected to remote destination"); + + self.inner + .write(&new_reply( + &ReplyError::Succeeded, + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0), + )) + .await + .context("Can't write successful reply")?; + + self.inner.flush().await.context("Can't flush the reply!")?; + + debug!("Wrote success"); + + transfer(&mut self.inner, outbound).await + } + + /// Bind to a random UDP port, wait for the traffic from + /// the client, and then forward the data to the remote addr. + async fn execute_command_udp_assoc(&mut self) -> Result<()> { + // The DST.ADDR and DST.PORT fields contain the address and port that + // the client expects to use to send UDP datagrams on for the + // association. The server MAY use this information to limit access + // to the association. + // @see Page 6, https://datatracker.ietf.org/doc/html/rfc1928. + // + // We do NOT limit the access from the client currently in this implementation. + let _not_used = self.target_addr.as_ref(); + + // Listen with UDP6 socket, so the client can connect to it with either + // IPv4 or IPv6. + let peer_sock = UdpSocket::bind("[::]:0").await?; + + // Respect the pre-populated reply IP address. + self.inner + .write(&new_reply( + &ReplyError::Succeeded, + SocketAddr::new( + self.reply_ip.context("invalid reply ip")?, + peer_sock.local_addr()?.port(), + ), + )) + .await + .context("Can't write successful reply")?; + + debug!("Wrote success"); + + transfer_udp(peer_sock).await?; + + Ok(()) + } + + pub fn target_addr(&self) -> Option<&TargetAddr> { + self.target_addr.as_ref() + } + + pub fn auth(&self) -> &AuthenticationMethod { + &self.auth + } + + pub fn cmd(&self) -> &Option { + &self.cmd + } + + /// Borrow the credentials of the user has authenticated with + pub fn get_credentials(&self) -> Option<&<::Item as Deref>::Target> + where + ::Item: Deref, + { + self.credentials.as_deref() + } + + /// Get the credentials of the user has authenticated with + pub fn take_credentials(&mut self) -> Option { + self.credentials.take() + } + + pub fn tcp_connector(&self) -> &C { + &self.tcp_connector + } +} + +/// Copy data between two peers +/// Using 2 different generators, because they could be different structs with same traits. +async fn transfer(mut inbound: I, mut outbound: O) -> Result<()> +where + I: AsyncRead + AsyncWrite + Unpin, + O: AsyncRead + AsyncWrite + Unpin, +{ + match tokio::io::copy_bidirectional(&mut inbound, &mut outbound).await { + Ok(res) => info!("transfer closed ({}, {})", res.0, res.1), + Err(err) => error!("transfer error: {:?}", err), + }; + + Ok(()) +} + +async fn handle_udp_request(inbound: &UdpSocket, outbound: &UdpSocket) -> Result<()> { + let mut buf = vec![0u8; 0x10000]; + loop { + let (size, client_addr) = inbound.recv_from(&mut buf).await?; + debug!("Server recieve udp from {}", client_addr); + inbound.connect(client_addr).await?; + + let (frag, target_addr, data) = parse_udp_request(&buf[..size]).await?; + + if frag != 0 { + debug!("Discard UDP frag packets sliently."); + return Ok(()); + } + + debug!("Server forward to packet to {}", target_addr); + let mut target_addr = target_addr + .to_socket_addrs()? + .next() + .context("unreachable")?; + + target_addr.set_ip(match target_addr.ip() { + std::net::IpAddr::V4(v4) => std::net::IpAddr::V6(v4.to_ipv6_mapped()), + v6 @ std::net::IpAddr::V6(_) => v6, + }); + outbound.send_to(data, target_addr).await?; + } +} + +async fn handle_udp_response(inbound: &UdpSocket, outbound: &UdpSocket) -> Result<()> { + let mut buf = vec![0u8; 0x10000]; + loop { + let (size, remote_addr) = outbound.recv_from(&mut buf).await?; + debug!("Recieve packet from {}", remote_addr); + + let mut data = new_udp_header(remote_addr)?; + data.extend_from_slice(&buf[..size]); + inbound.send(&data).await?; + } +} + +async fn transfer_udp(inbound: UdpSocket) -> Result<()> { + let outbound = UdpSocket::bind("[::]:0").await?; + + let req_fut = handle_udp_request(&inbound, &outbound); + let res_fut = handle_udp_response(&inbound, &outbound); + match try_join!(req_fut, res_fut) { + Ok(_) => {} + Err(error) => return Err(error), + } + + Ok(()) +} + +// Fixes the issue "cannot borrow data in dereference of `Pin<&mut >` as mutable" +// +// cf. https://users.rust-lang.org/t/take-in-impl-future-cannot-borrow-data-in-a-dereference-of-pin/52042 +impl Unpin for Socks5Socket where + T: AsyncRead + AsyncWrite + Unpin +{ +} + +/// Allow us to read directly from the struct +impl AsyncRead for Socks5Socket +where + T: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_read( + mut self: Pin<&mut Self>, + context: &mut std::task::Context, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut self.inner).poll_read(context, buf) + } +} + +/// Allow us to write directly into the struct +impl AsyncWrite for Socks5Socket +where + T: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_write( + mut self: Pin<&mut Self>, + context: &mut std::task::Context, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut self.inner).poll_write(context, buf) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + context: &mut std::task::Context, + ) -> Poll> { + Pin::new(&mut self.inner).poll_flush(context) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + context: &mut std::task::Context, + ) -> Poll> { + Pin::new(&mut self.inner).poll_shutdown(context) + } +} + +/// Generate reply code according to the RFC. +fn new_reply(error: &ReplyError, sock_addr: SocketAddr) -> Vec { + let (addr_type, mut ip_oct, mut port) = match sock_addr { + SocketAddr::V4(sock) => ( + consts::SOCKS5_ADDR_TYPE_IPV4, + sock.ip().octets().to_vec(), + sock.port().to_be_bytes().to_vec(), + ), + SocketAddr::V6(sock) => ( + consts::SOCKS5_ADDR_TYPE_IPV6, + sock.ip().octets().to_vec(), + sock.port().to_be_bytes().to_vec(), + ), + }; + + let mut reply = vec![ + consts::SOCKS5_VERSION, + error.as_u8(), // transform the error into byte code + 0x00, // reserved + addr_type, // address type (ipv4, v6, domain) + ]; + reply.append(&mut ip_oct); + reply.append(&mut port); + + reply +} diff --git a/rust/easytier/src/gateway/fast_socks5/util/mod.rs b/rust/easytier/src/gateway/fast_socks5/util/mod.rs new file mode 100644 index 0000000..e1c2f62 --- /dev/null +++ b/rust/easytier/src/gateway/fast_socks5/util/mod.rs @@ -0,0 +1,2 @@ +pub mod stream; +pub mod target_addr; diff --git a/rust/easytier/src/gateway/fast_socks5/util/stream.rs b/rust/easytier/src/gateway/fast_socks5/util/stream.rs new file mode 100644 index 0000000..e76b34a --- /dev/null +++ b/rust/easytier/src/gateway/fast_socks5/util/stream.rs @@ -0,0 +1,65 @@ +use std::time::Duration; +use tokio::io::ErrorKind as IOErrorKind; +use tokio::net::{TcpStream, ToSocketAddrs}; +use tokio::time::timeout; + +use crate::gateway::fast_socks5::{ReplyError, Result}; + +/// Easy to destructure bytes buffers by naming each fields: +/// +/// # Examples (before) +/// +/// ```ignore +/// let mut buf = [0u8; 2]; +/// stream.read_exact(&mut buf).await?; +/// let [version, method_len] = buf; +/// +/// assert_eq!(version, 0x05); +/// ``` +/// +/// # Examples (after) +/// +/// ```ignore +/// let [version, method_len] = read_exact!(stream, [0u8; 2]); +/// +/// assert_eq!(version, 0x05); +/// ``` +#[macro_export] +macro_rules! read_exact { + ($stream: expr, $array: expr) => {{ + let mut x = $array; + // $stream + // .read_exact(&mut x) + // .await + // .map_err(|_| io_err("lol"))?; + $stream.read_exact(&mut x).await.map(|_| x) + }}; +} + +pub async fn tcp_connect_with_timeout(addr: T, request_timeout_s: u64) -> Result +where + T: ToSocketAddrs, +{ + let fut = tcp_connect(addr); + match timeout(Duration::from_secs(request_timeout_s), fut).await { + Ok(result) => result, + Err(_) => Err(ReplyError::ConnectionTimeout.into()), + } +} + +pub async fn tcp_connect(addr: T) -> Result +where + T: ToSocketAddrs, +{ + match TcpStream::connect(addr).await { + Ok(o) => Ok(o), + Err(e) => match e.kind() { + // Match other TCP errors with ReplyError + IOErrorKind::ConnectionRefused => Err(ReplyError::ConnectionRefused.into()), + IOErrorKind::ConnectionAborted => Err(ReplyError::ConnectionNotAllowed.into()), + IOErrorKind::ConnectionReset => Err(ReplyError::ConnectionNotAllowed.into()), + IOErrorKind::NotConnected => Err(ReplyError::NetworkUnreachable.into()), + _ => Err(e.into()), // #[error("General failure")] ? + }, + } +} diff --git a/rust/easytier/src/gateway/fast_socks5/util/target_addr.rs b/rust/easytier/src/gateway/fast_socks5/util/target_addr.rs new file mode 100644 index 0000000..fa2dbbf --- /dev/null +++ b/rust/easytier/src/gateway/fast_socks5/util/target_addr.rs @@ -0,0 +1,244 @@ +use crate::gateway::fast_socks5::consts; +use crate::gateway::fast_socks5::consts::SOCKS5_ADDR_TYPE_IPV4; +use crate::gateway::fast_socks5::SocksError; +use crate::read_exact; + +use anyhow::Context; +use std::fmt; +use std::io; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; +use std::vec::IntoIter; +use thiserror::Error; +use tokio::io::{AsyncRead, AsyncReadExt}; +use tokio::net::lookup_host; + +use tracing::{debug, error}; + +/// SOCKS5 reply code +#[derive(Error, Debug)] +pub enum AddrError { + #[error("DNS Resolution failed")] + DNSResolutionFailed, + #[error("Can't read IPv4")] + IPv4Unreadable, + #[error("Can't read IPv6")] + IPv6Unreadable, + #[error("Can't read port number")] + PortNumberUnreadable, + #[error("Can't read domain len")] + DomainLenUnreadable, + #[error("Can't read Domain content")] + DomainContentUnreadable, + #[error("Malformed UTF-8")] + Utf8, + #[error("Unknown address type")] + IncorrectAddressType, + #[error("{0}")] + Custom(String), +} + +/// A description of a connection target. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum TargetAddr { + /// Connect to an IP address. + Ip(SocketAddr), + /// Connect to a fully qualified domain name. + /// + /// The domain name will be passed along to the proxy server and DNS lookup + /// will happen there. + Domain(String, u16), +} + +impl TargetAddr { + pub async fn resolve_dns(self) -> anyhow::Result { + match self { + TargetAddr::Ip(ip) => Ok(TargetAddr::Ip(ip)), + TargetAddr::Domain(domain, port) => { + debug!("Attempt to DNS resolve the domain {}...", &domain); + + let socket_addr = lookup_host((&domain[..], port)) + .await + .context(AddrError::DNSResolutionFailed)? + .next() + .ok_or(AddrError::Custom( + "Can't fetch DNS to the domain.".to_string(), + ))?; + debug!("domain name resolved to {}", socket_addr); + + // has been converted to an ip + Ok(TargetAddr::Ip(socket_addr)) + } + } + } + + pub fn is_ip(&self) -> bool { + match self { + TargetAddr::Ip(_) => true, + _ => false, + } + } + + pub fn is_domain(&self) -> bool { + !self.is_ip() + } + + pub fn to_be_bytes(&self) -> anyhow::Result> { + let mut buf = vec![]; + match self { + TargetAddr::Ip(SocketAddr::V4(addr)) => { + debug!("TargetAddr::IpV4"); + + buf.extend_from_slice(&[SOCKS5_ADDR_TYPE_IPV4]); + + debug!("addr ip {:?}", (*addr.ip()).octets()); + buf.extend_from_slice(&(addr.ip()).octets()); // ip + buf.extend_from_slice(&addr.port().to_be_bytes()); // port + } + TargetAddr::Ip(SocketAddr::V6(addr)) => { + debug!("TargetAddr::IpV6"); + buf.extend_from_slice(&[consts::SOCKS5_ADDR_TYPE_IPV6]); + + debug!("addr ip {:?}", (*addr.ip()).octets()); + buf.extend_from_slice(&(addr.ip()).octets()); // ip + buf.extend_from_slice(&addr.port().to_be_bytes()); // port + } + TargetAddr::Domain(ref domain, port) => { + debug!("TargetAddr::Domain"); + if domain.len() > u8::max_value() as usize { + return Err(SocksError::ExceededMaxDomainLen(domain.len()).into()); + } + buf.extend_from_slice(&[consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME, domain.len() as u8]); + buf.extend_from_slice(domain.as_bytes()); // domain content + buf.extend_from_slice(&port.to_be_bytes()); + // port content (.to_be_bytes() convert from u16 to u8 type) + } + } + Ok(buf) + } +} + +// async-std ToSocketAddrs doesn't supports external trait implementation +// @see https://github.com/async-rs/async-std/issues/539 +impl std::net::ToSocketAddrs for TargetAddr { + type Iter = IntoIter; + + fn to_socket_addrs(&self) -> io::Result> { + match *self { + TargetAddr::Ip(addr) => Ok(vec![addr].into_iter()), + TargetAddr::Domain(_, _) => Err(io::Error::new( + io::ErrorKind::Other, + "Domain name has to be explicitly resolved, please use TargetAddr::resolve_dns().", + )), + } + } +} + +impl fmt::Display for TargetAddr { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + TargetAddr::Ip(ref addr) => write!(f, "{}", addr), + TargetAddr::Domain(ref addr, ref port) => write!(f, "{}:{}", addr, port), + } + } +} + +/// A trait for objects that can be converted to `TargetAddr`. +pub trait ToTargetAddr { + /// Converts the value of `self` to a `TargetAddr`. + fn to_target_addr(&self) -> io::Result; +} + +impl<'a> ToTargetAddr for (&'a str, u16) { + fn to_target_addr(&self) -> io::Result { + // try to parse as an IP first + if let Ok(addr) = self.0.parse::() { + return (addr, self.1).to_target_addr(); + } + + if let Ok(addr) = self.0.parse::() { + return (addr, self.1).to_target_addr(); + } + + Ok(TargetAddr::Domain(self.0.to_owned(), self.1)) + } +} + +impl ToTargetAddr for SocketAddr { + fn to_target_addr(&self) -> io::Result { + Ok(TargetAddr::Ip(*self)) + } +} + +impl ToTargetAddr for SocketAddrV4 { + fn to_target_addr(&self) -> io::Result { + SocketAddr::V4(*self).to_target_addr() + } +} + +impl ToTargetAddr for SocketAddrV6 { + fn to_target_addr(&self) -> io::Result { + SocketAddr::V6(*self).to_target_addr() + } +} + +impl ToTargetAddr for (Ipv4Addr, u16) { + fn to_target_addr(&self) -> io::Result { + SocketAddrV4::new(self.0, self.1).to_target_addr() + } +} + +impl ToTargetAddr for (Ipv6Addr, u16) { + fn to_target_addr(&self) -> io::Result { + SocketAddrV6::new(self.0, self.1, 0, 0).to_target_addr() + } +} + +#[derive(Debug)] +pub enum Addr { + V4([u8; 4]), + V6([u8; 16]), + Domain(String), // Vec<[u8]> or Box<[u8]> or String ? +} + +/// This function is used by the client & the server +pub async fn read_address( + stream: &mut T, + atyp: u8, +) -> anyhow::Result { + let addr = match atyp { + consts::SOCKS5_ADDR_TYPE_IPV4 => { + debug!("Address type `IPv4`"); + Addr::V4(read_exact!(stream, [0u8; 4]).context(AddrError::IPv4Unreadable)?) + } + consts::SOCKS5_ADDR_TYPE_IPV6 => { + debug!("Address type `IPv6`"); + Addr::V6(read_exact!(stream, [0u8; 16]).context(AddrError::IPv6Unreadable)?) + } + consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME => { + debug!("Address type `domain`"); + let len = read_exact!(stream, [0]).context(AddrError::DomainLenUnreadable)?[0]; + let domain = read_exact!(stream, vec![0u8; len as usize]) + .context(AddrError::DomainContentUnreadable)?; + // make sure the bytes are correct utf8 string + let domain = String::from_utf8(domain).context(AddrError::Utf8)?; + + Addr::Domain(domain) + } + _ => return Err(anyhow::anyhow!(AddrError::IncorrectAddressType)), + }; + + // Find port number + let port = read_exact!(stream, [0u8; 2]).context(AddrError::PortNumberUnreadable)?; + // Convert (u8 * 2) into u16 + let port = (port[0] as u16) << 8 | port[1] as u16; + + // Merge ADDRESS + PORT into a TargetAddr + let addr: TargetAddr = match addr { + Addr::V4([a, b, c, d]) => (Ipv4Addr::new(a, b, c, d), port).to_target_addr()?, + Addr::V6(x) => (Ipv6Addr::from(x), port).to_target_addr()?, + Addr::Domain(domain) => TargetAddr::Domain(domain, port), + }; + + Ok(addr) +} diff --git a/rust/easytier/src/gateway/icmp_proxy.rs b/rust/easytier/src/gateway/icmp_proxy.rs new file mode 100644 index 0000000..55156e2 --- /dev/null +++ b/rust/easytier/src/gateway/icmp_proxy.rs @@ -0,0 +1,441 @@ +use std::{ + mem::MaybeUninit, + net::{IpAddr, Ipv4Addr, SocketAddrV4}, + sync::Arc, + thread, + time::Duration, +}; + +use anyhow::Context; +use pnet::packet::{ + icmp::{self, echo_reply::MutableEchoReplyPacket, IcmpCode, IcmpTypes, MutableIcmpPacket}, + ip::IpNextHeaderProtocols, + ipv4::Ipv4Packet, + Packet, +}; +use socket2::Socket; +use tokio::{ + sync::{mpsc::UnboundedSender, Mutex}, + task::JoinSet, +}; + +use tracing::Instrument; + +use crate::{ + common::{error::Error, global_ctx::ArcGlobalCtx, PeerId}, + peers::{peer_manager::PeerManager, PeerPacketFilter}, + tunnel::packet_def::{PacketType, ZCPacket}, +}; + +use super::{ + ip_reassembler::{compose_ipv4_packet, IpReassembler}, + CidrSet, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct IcmpNatKey { + dst_ip: std::net::IpAddr, + icmp_id: u16, + icmp_seq: u16, +} + +#[derive(Debug)] +struct IcmpNatEntry { + src_peer_id: PeerId, + my_peer_id: PeerId, + src_ip: IpAddr, + start_time: std::time::Instant, +} + +impl IcmpNatEntry { + fn new(src_peer_id: PeerId, my_peer_id: PeerId, src_ip: IpAddr) -> Result { + Ok(Self { + src_peer_id, + my_peer_id, + src_ip, + start_time: std::time::Instant::now(), + }) + } +} + +type IcmpNatTable = Arc>; +type NewPacketSender = tokio::sync::mpsc::UnboundedSender; +type NewPacketReceiver = tokio::sync::mpsc::UnboundedReceiver; + +#[derive(Debug)] +pub struct IcmpProxy { + global_ctx: ArcGlobalCtx, + peer_manager: Arc, + + cidr_set: CidrSet, + socket: std::sync::Mutex>, + + nat_table: IcmpNatTable, + + tasks: Mutex>, + + ip_resemmbler: Arc, + icmp_sender: Arc>>>, +} + +fn socket_recv(socket: &Socket, buf: &mut [MaybeUninit]) -> Result<(usize, IpAddr), Error> { + let (size, addr) = socket.recv_from(buf)?; + let addr = match addr.as_socket() { + None => IpAddr::V4(Ipv4Addr::UNSPECIFIED), + Some(add) => add.ip(), + }; + Ok((size, addr)) +} + +fn socket_recv_loop(socket: Socket, nat_table: IcmpNatTable, sender: UnboundedSender) { + let mut buf = [0u8; 8192]; + let data: &mut [MaybeUninit] = unsafe { std::mem::transmute(&mut buf[..]) }; + + loop { + let Ok((len, peer_ip)) = socket_recv(&socket, data) else { + continue; + }; + + if !peer_ip.is_ipv4() { + continue; + } + + let Some(ipv4_packet) = Ipv4Packet::new(&buf[..len]) else { + continue; + }; + + let Some(icmp_packet) = icmp::echo_reply::EchoReplyPacket::new(ipv4_packet.payload()) + else { + continue; + }; + + if icmp_packet.get_icmp_type() != IcmpTypes::EchoReply { + continue; + } + + let key = IcmpNatKey { + dst_ip: peer_ip, + icmp_id: icmp_packet.get_identifier(), + icmp_seq: icmp_packet.get_sequence_number(), + }; + + let Some((_, v)) = nat_table.remove(&key) else { + continue; + }; + + // send packet back to the peer where this request origin. + let IpAddr::V4(dest_ip) = v.src_ip else { + continue; + }; + + let src_v4 = ipv4_packet.get_source(); + let payload_len = len - ipv4_packet.get_header_length() as usize * 4; + let id = ipv4_packet.get_identification(); + let _ = compose_ipv4_packet( + &mut buf[..], + &src_v4, + &dest_ip, + IpNextHeaderProtocols::Icmp, + payload_len, + 1200, + id, + |buf| { + let mut p = ZCPacket::new_with_payload(buf); + p.fill_peer_manager_hdr( + v.my_peer_id.into(), + v.src_peer_id.into(), + PacketType::Data as u8, + ); + p.mut_peer_manager_header().unwrap().set_no_proxy(true); + + if let Err(e) = sender.send(p) { + tracing::error!("send icmp packet to peer failed: {:?}, may exiting..", e); + } + Ok(()) + }, + ); + } +} + +#[async_trait::async_trait] +impl PeerPacketFilter for IcmpProxy { + async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option { + if let Some(_) = self.try_handle_peer_packet(&packet).await { + return None; + } else { + return Some(packet); + } + } +} + +impl IcmpProxy { + pub fn new( + global_ctx: ArcGlobalCtx, + peer_manager: Arc, + ) -> Result, Error> { + let cidr_set = CidrSet::new(global_ctx.clone()); + let ret = Self { + global_ctx, + peer_manager, + cidr_set, + socket: std::sync::Mutex::new(None), + + nat_table: Arc::new(dashmap::DashMap::new()), + tasks: Mutex::new(JoinSet::new()), + + ip_resemmbler: Arc::new(IpReassembler::new(Duration::from_secs(10))), + icmp_sender: Arc::new(std::sync::Mutex::new(None)), + }; + + Ok(Arc::new(ret)) + } + + fn create_raw_socket(self: &Arc) -> Result { + let _g = self.global_ctx.net_ns.guard(); + let socket = socket2::Socket::new( + socket2::Domain::IPV4, + socket2::Type::RAW, + Some(socket2::Protocol::ICMPV4), + )?; + socket.bind(&socket2::SockAddr::from(SocketAddrV4::new( + std::net::Ipv4Addr::UNSPECIFIED, + 0, + )))?; + Ok(socket) + } + + pub async fn start(self: &Arc) -> Result<(), Error> { + let socket = self.create_raw_socket(); + match socket { + Ok(socket) => { + self.socket.lock().unwrap().replace(socket); + } + Err(e) => { + tracing::warn!("create icmp socket failed: {:?}", e); + if !self.global_ctx.no_tun() { + return Err(anyhow::anyhow!("create icmp socket failed: {:?}", e).into()); + } + } + } + + self.start_icmp_proxy().await?; + self.start_nat_table_cleaner().await?; + Ok(()) + } + + async fn start_nat_table_cleaner(self: &Arc) -> Result<(), Error> { + let nat_table = self.nat_table.clone(); + self.tasks.lock().await.spawn( + async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + nat_table.retain(|_, v| v.start_time.elapsed().as_secs() < 20); + } + } + .instrument(tracing::info_span!("icmp proxy nat table cleaner")), + ); + Ok(()) + } + + async fn start_icmp_proxy(self: &Arc) -> Result<(), Error> { + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel(); + self.icmp_sender.lock().unwrap().replace(sender.clone()); + if let Some(socket) = self.socket.lock().unwrap().as_ref() { + let socket = socket.try_clone()?; + let nat_table = self.nat_table.clone(); + thread::spawn(|| { + socket_recv_loop(socket, nat_table, sender); + }); + } + + let peer_manager = self.peer_manager.clone(); + self.tasks.lock().await.spawn( + async move { + while let Some(msg) = receiver.recv().await { + let hdr = msg.peer_manager_header().unwrap(); + let to_peer_id = hdr.to_peer_id.into(); + let ret = peer_manager.send_msg(msg, to_peer_id).await; + if ret.is_err() { + tracing::error!("send icmp packet to peer failed: {:?}", ret); + } + } + } + .instrument(tracing::info_span!("icmp proxy send loop")), + ); + + let ip_resembler = self.ip_resemmbler.clone(); + self.tasks.lock().await.spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(1)).await; + ip_resembler.remove_expired_packets(); + } + }); + + self.peer_manager + .add_packet_process_pipeline(Box::new(self.clone())) + .await; + Ok(()) + } + + fn send_icmp_packet( + &self, + dst_ip: Ipv4Addr, + icmp_packet: &icmp::echo_request::EchoRequestPacket, + ) -> Result<(), Error> { + self.socket + .lock() + .unwrap() + .as_ref() + .with_context(|| "icmp socket not created")? + .send_to( + icmp_packet.packet(), + &SocketAddrV4::new(dst_ip.into(), 0).into(), + )?; + + Ok(()) + } + + async fn send_icmp_reply_to_peer( + &self, + src_ip: &Ipv4Addr, + dst_ip: &Ipv4Addr, + src_peer_id: PeerId, + dst_peer_id: PeerId, + icmp_packet: &icmp::echo_request::EchoRequestPacket<'_>, + ) { + let mut buf = vec![0u8; icmp_packet.packet().len() + 20]; + let mut reply_packet = MutableEchoReplyPacket::new(&mut buf[20..]).unwrap(); + reply_packet.set_icmp_type(IcmpTypes::EchoReply); + reply_packet.set_icmp_code(IcmpCode::new(0)); + reply_packet.set_identifier(icmp_packet.get_identifier()); + reply_packet.set_sequence_number(icmp_packet.get_sequence_number()); + reply_packet.set_payload(icmp_packet.payload()); + + let mut icmp_packet = MutableIcmpPacket::new(&mut buf[20..]).unwrap(); + icmp_packet.set_checksum(icmp::checksum(&icmp_packet.to_immutable())); + + let len = buf.len() - 20; + let _ = compose_ipv4_packet( + &mut buf[..], + src_ip, + dst_ip, + IpNextHeaderProtocols::Icmp, + len, + 1200, + rand::random(), + |buf| { + let mut packet = ZCPacket::new_with_payload(buf); + packet.fill_peer_manager_hdr(src_peer_id, dst_peer_id, PacketType::Data as u8); + let _ = self + .icmp_sender + .lock() + .unwrap() + .as_ref() + .unwrap() + .send(packet); + Ok(()) + }, + ); + } + + async fn try_handle_peer_packet(&self, packet: &ZCPacket) -> Option<()> { + if self.cidr_set.is_empty() + && !self.global_ctx.enable_exit_node() + && !self.global_ctx.no_tun() + { + return None; + } + + let _ = self.global_ctx.get_ipv4()?; + let hdr = packet.peer_manager_header().unwrap(); + let is_exit_node = hdr.is_exit_node(); + + if hdr.packet_type != PacketType::Data as u8 || hdr.is_no_proxy() { + return None; + }; + + let ipv4 = Ipv4Packet::new(&packet.payload())?; + + if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Icmp + { + return None; + } + + if !self.cidr_set.contains_v4(ipv4.get_destination()) + && !is_exit_node + && !(self.global_ctx.no_tun() + && Some(ipv4.get_destination()) + == self + .global_ctx + .get_ipv4() + .as_ref() + .map(cidr::Ipv4Inet::address)) + { + return None; + } + + let resembled_buf: Option>; + let icmp_packet = if IpReassembler::is_packet_fragmented(&ipv4) { + resembled_buf = + self.ip_resemmbler + .add_fragment(ipv4.get_source(), ipv4.get_destination(), &ipv4); + if resembled_buf.is_none() { + return None; + }; + icmp::echo_request::EchoRequestPacket::new(resembled_buf.as_ref().unwrap())? + } else { + icmp::echo_request::EchoRequestPacket::new(&ipv4.payload())? + }; + + if icmp_packet.get_icmp_type() != IcmpTypes::EchoRequest { + // if it's other icmp type, just ignore it. may forwarding network to network replay packet. + tracing::trace!("unsupported icmp type: {:?}", icmp_packet.get_icmp_type()); + return None; + } + + if self.global_ctx.no_tun() + && Some(ipv4.get_destination()) + == self + .global_ctx + .get_ipv4() + .as_ref() + .map(cidr::Ipv4Inet::address) + { + self.send_icmp_reply_to_peer( + &ipv4.get_destination(), + &ipv4.get_source(), + hdr.to_peer_id.get(), + hdr.from_peer_id.get(), + &icmp_packet, + ) + .await; + return Some(()); + } + + let icmp_id = icmp_packet.get_identifier(); + let icmp_seq = icmp_packet.get_sequence_number(); + + let key = IcmpNatKey { + dst_ip: ipv4.get_destination().into(), + icmp_id, + icmp_seq, + }; + + let value = IcmpNatEntry::new( + hdr.from_peer_id.into(), + hdr.to_peer_id.into(), + ipv4.get_source().into(), + ) + .ok()?; + + if let Some(old) = self.nat_table.insert(key, value) { + tracing::info!("icmp nat table entry replaced: {:?}", old); + } + + if let Err(e) = self.send_icmp_packet(ipv4.get_destination(), &icmp_packet) { + tracing::error!("send icmp packet failed: {:?}", e); + } + + Some(()) + } +} diff --git a/rust/easytier/src/gateway/ip_reassembler.rs b/rust/easytier/src/gateway/ip_reassembler.rs new file mode 100644 index 0000000..7f20c9e --- /dev/null +++ b/rust/easytier/src/gateway/ip_reassembler.rs @@ -0,0 +1,299 @@ +use dashmap::DashMap; +use pnet::packet::ip::IpNextHeaderProtocol; +use pnet::packet::ipv4::{self, Ipv4Flags, Ipv4Packet, MutableIpv4Packet}; +use pnet::packet::Packet; +use std::net::Ipv4Addr; +use std::time::{Duration, Instant}; + +use crate::common::error::Error; + +#[derive(Debug, Clone)] +pub(crate) struct IpFragment { + id: u16, + offset: u16, + data: Vec, +} + +impl<'a> From<&Ipv4Packet<'a>> for IpFragment { + fn from(packet: &Ipv4Packet<'a>) -> Self { + let id = packet.get_identification(); + let offset = packet.get_fragment_offset() * 8; + let data = packet.payload().to_vec(); + IpFragment { id, offset, data } + } +} + +#[derive(Debug, Clone)] +struct IpPacket { + source: Ipv4Addr, + destination: Ipv4Addr, + total_length: Option, + fragments: Vec, +} + +impl IpPacket { + fn new(source: Ipv4Addr, destination: Ipv4Addr) -> Self { + IpPacket { + source, + destination, + total_length: None, + fragments: Vec::new(), + } + } + + fn add_fragment(&mut self, fragment: IpFragment) { + // make sure the fragment doesn't overlap with existing fragments + for f in &self.fragments { + if f.offset <= fragment.offset && fragment.offset < f.offset + f.data.len() as u16 { + return; + } + if fragment.offset <= f.offset + && f.offset < fragment.offset + fragment.data.len() as u16 + { + return; + } + } + self.fragments.push(fragment); + } + + fn is_complete(&self) -> bool { + if self.total_length.is_none() { + return false; + } + let mut total_length = 0; + for fragment in &self.fragments { + total_length += fragment.data.len() as u16; + } + tracing::trace!(?total_length, ?self.total_length, "ip resembler checking is_complete"); + Some(total_length) == self.total_length + } + + fn set_total_length(&mut self, total_length: u16) { + self.total_length = Some(total_length); + } + + fn assemble(&mut self) -> Option> { + if !self.is_complete() { + return None; + } + + // sort fragments by offset + self.fragments.sort_by_key(|f| f.offset); + + let mut packet = vec![0u8; self.total_length.unwrap() as usize]; + for fragment in &self.fragments { + let start = fragment.offset as usize; + let end = start + fragment.data.len(); + packet[start..end].copy_from_slice(&fragment.data); + } + + Some(packet) + } +} + +#[derive(Hash, Eq, PartialEq, Clone, Debug)] +struct IpResemblerKey { + source: Ipv4Addr, + destination: Ipv4Addr, + id: u16, +} + +#[derive(Debug)] +struct IpResemblerValue { + packet: IpPacket, + timestamp: Instant, +} + +#[derive(Debug)] +pub(crate) struct IpReassembler { + packets: DashMap, + timeout: Duration, +} + +impl IpReassembler { + pub fn new(timeout: Duration) -> Self { + IpReassembler { + packets: DashMap::new(), + timeout, + } + } + + pub fn is_packet_fragmented(packet: &Ipv4Packet) -> bool { + packet.get_fragment_offset() != 0 || packet.get_flags() & Ipv4Flags::MoreFragments != 0 + } + + pub fn is_last_fragment(packet: &Ipv4Packet) -> bool { + packet.get_flags() & Ipv4Flags::MoreFragments == 0 + } + + pub fn add_fragment( + &self, + source: Ipv4Addr, + destination: Ipv4Addr, + packet: &Ipv4Packet, + ) -> Option> { + let id = packet.get_identification(); + let total_length = packet.get_total_length() - packet.get_header_length() as u16 * 4; + if total_length != packet.payload().len() as u16 { + tracing::trace!( + ?packet, + ?total_length, + payload_len = ?packet.payload().len(), + "unexpected total length", + ); + return None; + } + + let fragment: IpFragment = packet.into(); + let key = IpResemblerKey { + source, + destination, + id, + }; + + let mut entry = self.packets.entry(key.clone()).or_insert_with(|| { + let packet = IpPacket::new(source, destination); + let timestamp = Instant::now(); + IpResemblerValue { packet, timestamp } + }); + let value_mut = entry.value_mut(); + + if Self::is_last_fragment(packet) { + value_mut + .packet + .set_total_length(total_length + fragment.offset); + } + + value_mut.packet.add_fragment(fragment); + if let Some(data) = value_mut.packet.assemble() { + drop(entry); + self.packets.remove(&key); + Some(data) + } else { + value_mut.timestamp = Instant::now(); + None + } + } + + pub fn remove_expired_packets(&self) { + let timeout = self.timeout; + self.packets.retain(|_, v| v.timestamp.elapsed() <= timeout); + } +} + +// ip payload should be in buf[20..] +pub fn compose_ipv4_packet( + buf: &mut [u8], + src_v4: &Ipv4Addr, + dst_v4: &Ipv4Addr, + next_protocol: IpNextHeaderProtocol, + payload_len: usize, + payload_mtu: usize, + ip_id: u16, + cb: F, +) -> Result<(), Error> +where + F: Fn(&[u8]) -> Result<(), Error>, +{ + let total_pieces = (payload_len + payload_mtu - 1) / payload_mtu; + let mut buf_offset = 0; + let mut fragment_offset = 0; + let mut cur_piece = 0; + while fragment_offset < payload_len { + let next_fragment_offset = std::cmp::min(fragment_offset + payload_mtu, payload_len); + let fragment_len = next_fragment_offset - fragment_offset; + let mut ipv4_packet = + MutableIpv4Packet::new(&mut buf[buf_offset..buf_offset + fragment_len + 20]).unwrap(); + ipv4_packet.set_version(4); + ipv4_packet.set_header_length(5); + ipv4_packet.set_total_length((fragment_len + 20) as u16); + ipv4_packet.set_identification(ip_id); + if total_pieces > 1 { + if cur_piece != total_pieces - 1 { + ipv4_packet.set_flags(Ipv4Flags::MoreFragments); + } else { + ipv4_packet.set_flags(0); + } + assert_eq!(0, fragment_offset % 8); + ipv4_packet.set_fragment_offset(fragment_offset as u16 / 8); + } else { + ipv4_packet.set_flags(Ipv4Flags::DontFragment); + ipv4_packet.set_fragment_offset(0); + } + ipv4_packet.set_ecn(0); + ipv4_packet.set_dscp(0); + ipv4_packet.set_ttl(32); + ipv4_packet.set_source(src_v4.clone()); + ipv4_packet.set_destination(dst_v4.clone()); + ipv4_packet.set_next_level_protocol(next_protocol); + ipv4_packet.set_checksum(ipv4::checksum(&ipv4_packet.to_immutable())); + + tracing::trace!(?ipv4_packet, "udp nat packet response send"); + + cb(ipv4_packet.packet())?; + + buf_offset += next_fragment_offset - fragment_offset; + fragment_offset = next_fragment_offset; + cur_piece += 1; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resembler() { + let raw_packets = vec![ + // last packet + vec![ + 0x45, 0x00, 0x00, 0x1c, 0x1c, 0x46, 0x20, 0x01, 0x40, 0x06, 0xb1, 0xe6, 0xc0, 0xa8, + 0x00, 0x01, 0xc0, 0xa8, 0x00, 0x02, 0x04, 0x05, 0x06, 0x07, 0x04, 0x05, 0x06, 0x07, + ], + // 1st packet + vec![ + 0x45, 0x00, 0x00, 0x1c, 0x1c, 0x46, 0x00, 0x02, 0x40, 0x06, 0xb1, 0xe6, 0xc0, 0xa8, + 0x00, 0x01, 0xc0, 0xa8, 0x00, 0x02, 0x08, 0x09, 0x0a, 0x0b, 0x04, 0x05, 0x06, 0x07, + ], + // 2nd packet + vec![ + 0x45, 0x00, 0x00, 0x1c, 0x1c, 0x46, 0x20, 0x00, 0x40, 0x06, 0xb1, 0xe6, 0xc0, 0xa8, + 0x00, 0x01, 0xc0, 0xa8, 0x00, 0x02, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + ], + // expired packet + vec![ + 0x45, 0x00, 0x00, 0x1c, 0x1c, 0x47, 0x20, 0x00, 0x40, 0x06, 0xb1, 0xe6, 0xc0, 0xa8, + 0x00, 0x01, 0xc0, 0xa8, 0x00, 0x02, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + ], + ]; + + let source = "192.168.0.1".parse().unwrap(); + let destination = "192.168.0.2".parse().unwrap(); + let resembler = IpReassembler::new(Duration::from_secs(1)); + + for (idx, raw_packet) in raw_packets.iter().enumerate() { + if let Some(packet) = Ipv4Packet::new(&raw_packet) { + let ret = resembler.add_fragment(source, destination, &packet); + if idx != 2 { + assert!(ret.is_none()); + } else { + assert!(ret.is_some()); + } + println!( + "packet: {:?}, ret: {:?}, palyload_len: {}", + packet, + ret, + packet.payload().len() + ); + } + } + + resembler.remove_expired_packets(); + assert_eq!(1, resembler.packets.len()); + + std::thread::sleep(Duration::from_secs(2)); + resembler.remove_expired_packets(); + assert_eq!(0, resembler.packets.len()); + } +} diff --git a/rust/easytier/src/gateway/kcp_proxy.rs b/rust/easytier/src/gateway/kcp_proxy.rs new file mode 100644 index 0000000..9bd52f4 --- /dev/null +++ b/rust/easytier/src/gateway/kcp_proxy.rs @@ -0,0 +1,441 @@ +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::{Arc, Weak}, + time::Duration, +}; + +use anyhow::Context; +use bytes::Bytes; +use dashmap::DashMap; +use kcp_sys::{ + endpoint::{ConnId, KcpEndpoint, KcpPacketReceiver}, + ffi_safe::KcpConfig, + packet_def::KcpPacket, + stream::KcpStream, +}; +use pnet::packet::{ + ip::IpNextHeaderProtocols, + ipv4::Ipv4Packet, + tcp::{TcpFlags, TcpPacket}, + Packet as _, +}; +use prost::Message; +use tokio::{io::copy_bidirectional, task::JoinSet}; + +use super::{ + tcp_proxy::{NatDstConnector, NatDstTcpConnector, TcpProxy}, + CidrSet, +}; +use crate::{ + common::{ + error::Result, + global_ctx::{ArcGlobalCtx, GlobalCtx}, + }, + peers::{peer_manager::PeerManager, NicPacketFilter, PeerPacketFilter}, + proto::{ + cli::{ + ListTcpProxyEntryRequest, ListTcpProxyEntryResponse, TcpProxyEntry, TcpProxyEntryState, + TcpProxyEntryTransportType, TcpProxyRpc, + }, + peer_rpc::KcpConnData, + rpc_types::{self, controller::BaseController}, + }, + tunnel::packet_def::{PacketType, PeerManagerHeader, ZCPacket}, +}; + +fn create_kcp_endpoint() -> KcpEndpoint { + let mut kcp_endpoint = KcpEndpoint::new(); + kcp_endpoint.set_kcp_config_factory(Box::new(|conv| { + let mut cfg = KcpConfig::new_turbo(conv); + cfg.interval = Some(5); + cfg + })); + kcp_endpoint +} + +struct KcpEndpointFilter { + kcp_endpoint: Arc, + is_src: bool, +} + +#[async_trait::async_trait] +impl PeerPacketFilter for KcpEndpointFilter { + async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option { + let t = packet.peer_manager_header().unwrap().packet_type; + if t == PacketType::KcpSrc as u8 && !self.is_src { + } else if t == PacketType::KcpDst as u8 && self.is_src { + } else { + return Some(packet); + } + + let _ = self + .kcp_endpoint + .input_sender_ref() + .send(KcpPacket::from(packet.payload_bytes())) + .await; + + None + } +} + +#[tracing::instrument] +async fn handle_kcp_output( + peer_mgr: Arc, + mut output_receiver: KcpPacketReceiver, + is_src: bool, +) { + while let Some(packet) = output_receiver.recv().await { + let dst_peer_id = if is_src { + packet.header().dst_session_id() + } else { + packet.header().src_session_id() + }; + let packet_type = if is_src { + PacketType::KcpSrc as u8 + } else { + PacketType::KcpDst as u8 + }; + let mut packet = ZCPacket::new_with_payload(&packet.inner().freeze()); + packet.fill_peer_manager_hdr(peer_mgr.my_peer_id(), dst_peer_id, packet_type as u8); + + if let Err(e) = peer_mgr.send_msg(packet, dst_peer_id).await { + tracing::error!("failed to send kcp packet to peer: {:?}", e); + } + } +} + +#[derive(Debug, Clone)] +pub struct NatDstKcpConnector { + kcp_endpoint: Arc, + peer_mgr: Arc, +} + +#[async_trait::async_trait] +impl NatDstConnector for NatDstKcpConnector { + type DstStream = KcpStream; + + async fn connect(&self, src: SocketAddr, nat_dst: SocketAddr) -> Result { + let conn_data = KcpConnData { + src: Some(src.into()), + dst: Some(nat_dst.into()), + }; + + let (dst_peers, _) = match nat_dst { + SocketAddr::V4(addr) => { + let ip = addr.ip(); + self.peer_mgr.get_msg_dst_peer(&ip).await + } + SocketAddr::V6(_) => return Err(anyhow::anyhow!("ipv6 is not supported").into()), + }; + + tracing::trace!("kcp nat dst: {:?}, dst peers: {:?}", nat_dst, dst_peers); + + if dst_peers.len() != 1 { + return Err(anyhow::anyhow!("no dst peer found for nat dst: {}", nat_dst).into()); + } + + let ret = self + .kcp_endpoint + .connect( + Duration::from_secs(10), + self.peer_mgr.my_peer_id(), + dst_peers[0], + Bytes::from(conn_data.encode_to_vec()), + ) + .await + .with_context(|| format!("failed to connect to nat dst: {}", nat_dst.to_string()))?; + + let stream = KcpStream::new(&self.kcp_endpoint, ret) + .ok_or(anyhow::anyhow!("failed to create kcp stream"))?; + + Ok(stream) + } + + fn check_packet_from_peer_fast(&self, _cidr_set: &CidrSet, _global_ctx: &GlobalCtx) -> bool { + true + } + + fn check_packet_from_peer( + &self, + _cidr_set: &CidrSet, + _global_ctx: &GlobalCtx, + hdr: &PeerManagerHeader, + _ipv4: &Ipv4Packet, + ) -> bool { + return hdr.from_peer_id == hdr.to_peer_id; + } + + fn transport_type(&self) -> TcpProxyEntryTransportType { + TcpProxyEntryTransportType::Kcp + } +} + +#[derive(Clone)] +struct TcpProxyForKcpSrc(Arc>); + +pub struct KcpProxySrc { + kcp_endpoint: Arc, + peer_manager: Arc, + + tcp_proxy: TcpProxyForKcpSrc, + tasks: JoinSet<()>, +} + +impl TcpProxyForKcpSrc { + async fn check_dst_allow_kcp_input(&self, dst_ip: &Ipv4Addr) -> bool { + let peer_map: Arc = + self.0.get_peer_manager().get_peer_map(); + let Some(dst_peer_id) = peer_map.get_peer_id_by_ipv4(dst_ip).await else { + return false; + }; + let Some(feature_flag) = peer_map.get_peer_feature_flag(dst_peer_id).await else { + return false; + }; + feature_flag.kcp_input + } +} + +#[async_trait::async_trait] +impl NicPacketFilter for TcpProxyForKcpSrc { + async fn try_process_packet_from_nic(&self, zc_packet: &mut ZCPacket) -> bool { + let ret = self.0.try_process_packet_from_nic(zc_packet).await; + if ret { + return true; + } + + let data = zc_packet.payload(); + let ip_packet = Ipv4Packet::new(data).unwrap(); + if ip_packet.get_version() != 4 + || ip_packet.get_next_level_protocol() != IpNextHeaderProtocols::Tcp + { + return false; + } + + // if no connection is established, only allow SYN packet + let tcp_packet = TcpPacket::new(ip_packet.payload()).unwrap(); + let is_syn = tcp_packet.get_flags() & TcpFlags::SYN != 0 + && tcp_packet.get_flags() & TcpFlags::ACK == 0; + if is_syn { + // only check dst feature flag when SYN packet + if !self + .check_dst_allow_kcp_input(&ip_packet.get_destination()) + .await + { + return false; + } + } else { + // if not syn packet, only allow established connection + if !self.0.is_tcp_proxy_connection(SocketAddr::new( + IpAddr::V4(ip_packet.get_source()), + tcp_packet.get_source(), + )) { + return false; + } + } + + if let Some(my_ipv4) = self.0.get_global_ctx().get_ipv4() { + // this is a net-to-net packet, only allow it when smoltcp is enabled + // because the syn-ack packet will not be through and handled by the tun device when + // the source ip is in the local network + if ip_packet.get_source() != my_ipv4.address() && !self.0.is_smoltcp_enabled() { + return false; + } + }; + + zc_packet.mut_peer_manager_header().unwrap().to_peer_id = self.0.get_my_peer_id().into(); + + true + } +} + +impl KcpProxySrc { + pub async fn new(peer_manager: Arc) -> Self { + let mut kcp_endpoint = create_kcp_endpoint(); + kcp_endpoint.run().await; + + let output_receiver = kcp_endpoint.output_receiver().unwrap(); + let mut tasks = JoinSet::new(); + + tasks.spawn(handle_kcp_output( + peer_manager.clone(), + output_receiver, + true, + )); + + let kcp_endpoint = Arc::new(kcp_endpoint); + + let tcp_proxy = TcpProxy::new( + peer_manager.clone(), + NatDstKcpConnector { + kcp_endpoint: kcp_endpoint.clone(), + peer_mgr: peer_manager.clone(), + }, + ); + + Self { + kcp_endpoint, + peer_manager, + tcp_proxy: TcpProxyForKcpSrc(tcp_proxy), + tasks, + } + } + + pub async fn start(&self) { + self.peer_manager + .add_nic_packet_process_pipeline(Box::new(self.tcp_proxy.clone())) + .await; + self.peer_manager + .add_packet_process_pipeline(Box::new(self.tcp_proxy.0.clone())) + .await; + self.peer_manager + .add_packet_process_pipeline(Box::new(KcpEndpointFilter { + kcp_endpoint: self.kcp_endpoint.clone(), + is_src: true, + })) + .await; + self.tcp_proxy.0.start(false).await.unwrap(); + } + + pub fn get_tcp_proxy(&self) -> Arc> { + self.tcp_proxy.0.clone() + } +} + +pub struct KcpProxyDst { + kcp_endpoint: Arc, + peer_manager: Arc, + proxy_entries: Arc>, + tasks: JoinSet<()>, +} + +impl KcpProxyDst { + pub async fn new(peer_manager: Arc) -> Self { + let mut kcp_endpoint = create_kcp_endpoint(); + kcp_endpoint.run().await; + + let mut tasks = JoinSet::new(); + let output_receiver = kcp_endpoint.output_receiver().unwrap(); + tasks.spawn(handle_kcp_output( + peer_manager.clone(), + output_receiver, + false, + )); + + Self { + kcp_endpoint: Arc::new(kcp_endpoint), + peer_manager, + proxy_entries: Arc::new(DashMap::new()), + tasks, + } + } + + #[tracing::instrument(ret)] + async fn handle_one_in_stream( + mut kcp_stream: KcpStream, + global_ctx: ArcGlobalCtx, + proxy_entries: Arc>, + ) -> Result<()> { + let mut conn_data = kcp_stream.conn_data().clone(); + let parsed_conn_data = KcpConnData::decode(&mut conn_data) + .with_context(|| format!("failed to decode kcp conn data: {:?}", conn_data))?; + let mut dst_socket: SocketAddr = parsed_conn_data + .dst + .ok_or(anyhow::anyhow!( + "failed to get dst socket from kcp conn data: {:?}", + parsed_conn_data + ))? + .into(); + + let conn_id = kcp_stream.conn_id(); + proxy_entries.insert( + conn_id, + TcpProxyEntry { + src: parsed_conn_data.src, + dst: parsed_conn_data.dst, + start_time: chrono::Local::now().timestamp() as u64, + state: TcpProxyEntryState::ConnectingDst.into(), + transport_type: TcpProxyEntryTransportType::Kcp.into(), + }, + ); + crate::defer! { + proxy_entries.remove(&conn_id); + } + + if Some(dst_socket.ip()) == global_ctx.get_ipv4().map(|ip| IpAddr::V4(ip.address())) + && global_ctx.no_tun() + { + dst_socket = format!("127.0.0.1:{}", dst_socket.port()).parse().unwrap(); + } + + tracing::debug!("kcp connect to dst socket: {:?}", dst_socket); + + let _g = global_ctx.net_ns.guard(); + let connector = NatDstTcpConnector {}; + let mut ret = connector + .connect("0.0.0.0:0".parse().unwrap(), dst_socket) + .await?; + + if let Some(mut e) = proxy_entries.get_mut(&kcp_stream.conn_id()) { + e.state = TcpProxyEntryState::Connected.into(); + } + + copy_bidirectional(&mut ret, &mut kcp_stream).await?; + Ok(()) + } + + async fn run_accept_task(&mut self) { + let kcp_endpoint = self.kcp_endpoint.clone(); + let global_ctx = self.peer_manager.get_global_ctx().clone(); + let proxy_entries = self.proxy_entries.clone(); + self.tasks.spawn(async move { + while let Ok(conn) = kcp_endpoint.accept().await { + let stream = KcpStream::new(&kcp_endpoint, conn) + .ok_or(anyhow::anyhow!("failed to create kcp stream")) + .unwrap(); + + let global_ctx = global_ctx.clone(); + let proxy_entries = proxy_entries.clone(); + tokio::spawn(async move { + let _ = Self::handle_one_in_stream(stream, global_ctx, proxy_entries).await; + }); + } + }); + } + + pub async fn start(&mut self) { + self.run_accept_task().await; + self.peer_manager + .add_packet_process_pipeline(Box::new(KcpEndpointFilter { + kcp_endpoint: self.kcp_endpoint.clone(), + is_src: false, + })) + .await; + } +} + +#[derive(Clone)] +pub struct KcpProxyDstRpcService(Weak>); + +impl KcpProxyDstRpcService { + pub fn new(kcp_proxy_dst: &KcpProxyDst) -> Self { + Self(Arc::downgrade(&kcp_proxy_dst.proxy_entries)) + } +} + +#[async_trait::async_trait] +impl TcpProxyRpc for KcpProxyDstRpcService { + type Controller = BaseController; + async fn list_tcp_proxy_entry( + &self, + _: BaseController, + _request: ListTcpProxyEntryRequest, // Accept request of type HelloRequest + ) -> std::result::Result { + let mut reply = ListTcpProxyEntryResponse::default(); + if let Some(tcp_proxy) = self.0.upgrade() { + for item in tcp_proxy.iter() { + reply.entries.push(item.value().clone()); + } + } + Ok(reply) + } +} diff --git a/rust/easytier/src/gateway/mod.rs b/rust/easytier/src/gateway/mod.rs new file mode 100644 index 0000000..030b5b3 --- /dev/null +++ b/rust/easytier/src/gateway/mod.rs @@ -0,0 +1,71 @@ +use std::sync::{Arc, Mutex}; +use tokio::task::JoinSet; + +use crate::common::global_ctx::ArcGlobalCtx; + +pub mod icmp_proxy; +pub mod ip_reassembler; +pub mod tcp_proxy; +#[cfg(feature = "smoltcp")] +pub mod tokio_smoltcp; +pub mod udp_proxy; + +#[cfg(feature = "socks5")] +pub mod fast_socks5; +#[cfg(feature = "socks5")] +pub mod socks5; + +pub mod kcp_proxy; + +#[derive(Debug)] +pub(crate) struct CidrSet { + global_ctx: ArcGlobalCtx, + cidr_set: Arc>>, + tasks: JoinSet<()>, +} + +impl CidrSet { + pub fn new(global_ctx: ArcGlobalCtx) -> Self { + let mut ret = Self { + global_ctx, + cidr_set: Arc::new(Mutex::new(vec![])), + tasks: JoinSet::new(), + }; + ret.run_cidr_updater(); + ret + } + + fn run_cidr_updater(&mut self) { + let global_ctx = self.global_ctx.clone(); + let cidr_set = self.cidr_set.clone(); + self.tasks.spawn(async move { + let mut last_cidrs = vec![]; + loop { + let cidrs = global_ctx.get_proxy_cidrs(); + if cidrs != last_cidrs { + last_cidrs = cidrs.clone(); + cidr_set.lock().unwrap().clear(); + for cidr in cidrs.iter() { + cidr_set.lock().unwrap().push(cidr.clone()); + } + } + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + }); + } + + pub fn contains_v4(&self, ip: std::net::Ipv4Addr) -> bool { + let ip = ip.into(); + let s = self.cidr_set.lock().unwrap(); + for cidr in s.iter() { + if cidr.contains(&ip) { + return true; + } + } + false + } + + pub fn is_empty(&self) -> bool { + self.cidr_set.lock().unwrap().is_empty() + } +} diff --git a/rust/easytier/src/gateway/socks5.rs b/rust/easytier/src/gateway/socks5.rs new file mode 100644 index 0000000..c3635f4 --- /dev/null +++ b/rust/easytier/src/gateway/socks5.rs @@ -0,0 +1,418 @@ +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::Arc, + time::Duration, +}; + +use crate::{ + gateway::{ + fast_socks5::{ + server::{ + AcceptAuthentication, AsyncTcpConnector, Config, SimpleUserPassword, Socks5Socket, + }, + util::stream::tcp_connect_with_timeout, + }, + tokio_smoltcp::TcpStream, + }, + tunnel::packet_def::PacketType, +}; +use anyhow::Context; +use dashmap::DashSet; +use pnet::packet::{ip::IpNextHeaderProtocols, ipv4::Ipv4Packet, tcp::TcpPacket, Packet}; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + select, +}; +use tokio::{ + net::TcpListener, + sync::{mpsc, Mutex}, + task::JoinSet, + time::timeout, +}; + +use crate::{ + common::{error::Error, global_ctx::GlobalCtx}, + gateway::tokio_smoltcp::{channel_device, Net, NetConfig}, + peers::{peer_manager::PeerManager, PeerPacketFilter}, + tunnel::packet_def::ZCPacket, +}; + +enum SocksTcpStream { + TcpStream(tokio::net::TcpStream), + SmolTcpStream(TcpStream), +} + +impl AsyncRead for SocksTcpStream { + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + match self.get_mut() { + SocksTcpStream::TcpStream(ref mut stream) => { + std::pin::Pin::new(stream).poll_read(cx, buf) + } + SocksTcpStream::SmolTcpStream(ref mut stream) => { + std::pin::Pin::new(stream).poll_read(cx, buf) + } + } + } +} + +impl AsyncWrite for SocksTcpStream { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + match self.get_mut() { + SocksTcpStream::TcpStream(ref mut stream) => { + std::pin::Pin::new(stream).poll_write(cx, buf) + } + SocksTcpStream::SmolTcpStream(ref mut stream) => { + std::pin::Pin::new(stream).poll_write(cx, buf) + } + } + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.get_mut() { + SocksTcpStream::TcpStream(ref mut stream) => std::pin::Pin::new(stream).poll_flush(cx), + SocksTcpStream::SmolTcpStream(ref mut stream) => { + std::pin::Pin::new(stream).poll_flush(cx) + } + } + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.get_mut() { + SocksTcpStream::TcpStream(ref mut stream) => { + std::pin::Pin::new(stream).poll_shutdown(cx) + } + SocksTcpStream::SmolTcpStream(ref mut stream) => { + std::pin::Pin::new(stream).poll_shutdown(cx) + } + } + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +struct Socks5Entry { + src: SocketAddr, + dst: SocketAddr, +} + +type Socks5EntrySet = Arc>; + +struct Socks5ServerNet { + ipv4_addr: cidr::Ipv4Inet, + auth: Option, + + smoltcp_net: Arc, + forward_tasks: Arc>>, + + entries: Socks5EntrySet, +} + +impl Socks5ServerNet { + pub fn new( + ipv4_addr: cidr::Ipv4Inet, + auth: Option, + peer_manager: Arc, + packet_recv: Arc>>, + entries: Socks5EntrySet, + ) -> Self { + let mut forward_tasks = JoinSet::new(); + let mut cap = smoltcp::phy::DeviceCapabilities::default(); + cap.max_transmission_unit = 1280; + cap.medium = smoltcp::phy::Medium::Ip; + let (dev, stack_sink, mut stack_stream) = channel_device::ChannelDevice::new(cap); + + let packet_recv = packet_recv.clone(); + forward_tasks.spawn(async move { + let mut smoltcp_stack_receiver = packet_recv.lock().await; + while let Some(packet) = smoltcp_stack_receiver.recv().await { + tracing::trace!(?packet, "receive from peer send to smoltcp packet"); + if let Err(e) = stack_sink.send(Ok(packet.payload().to_vec())).await { + tracing::error!("send to smoltcp stack failed: {:?}", e); + } + } + tracing::error!("smoltcp stack sink exited"); + panic!("smoltcp stack sink exited"); + }); + + forward_tasks.spawn(async move { + while let Some(data) = stack_stream.recv().await { + tracing::trace!( + ?data, + "receive from smoltcp stack and send to peer mgr packet" + ); + let Some(ipv4) = Ipv4Packet::new(&data) else { + tracing::error!(?data, "smoltcp stack stream get non ipv4 packet"); + continue; + }; + + let dst = ipv4.get_destination(); + let packet = ZCPacket::new_with_payload(&data); + if let Err(e) = peer_manager.send_msg_ipv4(packet, dst).await { + tracing::error!("send to peer failed in smoltcp sender: {:?}", e); + } + } + tracing::error!("smoltcp stack stream exited"); + panic!("smoltcp stack stream exited"); + }); + + let interface_config = smoltcp::iface::Config::new(smoltcp::wire::HardwareAddress::Ip); + let net = Net::new( + dev, + NetConfig::new( + interface_config, + format!("{}/{}", ipv4_addr.address(), ipv4_addr.network_length()) + .parse() + .unwrap(), + vec![format!("{}", ipv4_addr.address()).parse().unwrap()], + ), + ); + + Self { + ipv4_addr, + auth, + + smoltcp_net: Arc::new(net), + forward_tasks: Arc::new(std::sync::Mutex::new(forward_tasks)), + + entries, + } + } + + fn handle_tcp_stream(&self, stream: tokio::net::TcpStream) { + let mut config = Config::::default(); + config.set_request_timeout(10); + config.set_skip_auth(false); + config.set_allow_no_auth(true); + + struct SmolTcpConnector( + Arc, + Socks5EntrySet, + std::sync::Mutex>, + ); + + #[async_trait::async_trait] + impl AsyncTcpConnector for SmolTcpConnector { + type S = SocksTcpStream; + + async fn tcp_connect( + &self, + addr: SocketAddr, + timeout_s: u64, + ) -> crate::gateway::fast_socks5::Result { + let local_addr = self.0.get_address(); + let port = self.0.get_port(); + + let entry = Socks5Entry { + src: SocketAddr::new(local_addr, port), + dst: addr, + }; + *self.2.lock().unwrap() = Some(entry.clone()); + self.1.insert(entry); + + if addr.ip() == local_addr { + let modified_addr = + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), addr.port()); + + Ok(SocksTcpStream::TcpStream( + tcp_connect_with_timeout(modified_addr, timeout_s).await?, + )) + } else { + let remote_socket = timeout( + Duration::from_secs(timeout_s), + self.0.tcp_connect(addr, port), + ) + .await + .with_context(|| "connect to remote timeout")?; + + Ok(SocksTcpStream::SmolTcpStream(remote_socket.map_err( + |e| super::fast_socks5::SocksError::Other(e.into()), + )?)) + } + } + } + + impl Drop for SmolTcpConnector { + fn drop(&mut self) { + if let Some(entry) = self.2.lock().unwrap().take() { + self.1.remove(&entry); + } + } + } + + let socket = Socks5Socket::new( + stream, + Arc::new(config), + SmolTcpConnector( + self.smoltcp_net.clone(), + self.entries.clone(), + std::sync::Mutex::new(None), + ), + ); + + self.forward_tasks.lock().unwrap().spawn(async move { + match socket.upgrade_to_socks5().await { + Ok(_) => { + tracing::info!("socks5 handle success"); + } + Err(e) => { + tracing::error!("socks5 handshake failed: {:?}", e); + } + }; + }); + } +} + +pub struct Socks5Server { + global_ctx: Arc, + peer_manager: Arc, + auth: Option, + + tasks: Arc>>, + packet_sender: mpsc::Sender, + packet_recv: Arc>>, + + net: Arc>>, + entries: Socks5EntrySet, +} + +#[async_trait::async_trait] +impl PeerPacketFilter for Socks5Server { + async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option { + let hdr = packet.peer_manager_header().unwrap(); + if hdr.packet_type != PacketType::Data as u8 { + return Some(packet); + }; + + let payload_bytes = packet.payload(); + + let ipv4 = Ipv4Packet::new(payload_bytes).unwrap(); + if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Tcp { + return Some(packet); + } + + let tcp_packet = TcpPacket::new(ipv4.payload()).unwrap(); + let entry = Socks5Entry { + dst: SocketAddr::new(ipv4.get_source().into(), tcp_packet.get_source()), + src: SocketAddr::new(ipv4.get_destination().into(), tcp_packet.get_destination()), + }; + + if !self.entries.contains(&entry) { + return Some(packet); + } + + let _ = self.packet_sender.try_send(packet).ok(); + return None; + } +} + +impl Socks5Server { + pub fn new( + global_ctx: Arc, + peer_manager: Arc, + auth: Option, + ) -> Arc { + let (packet_sender, packet_recv) = mpsc::channel(1024); + Arc::new(Self { + global_ctx, + peer_manager, + auth, + + tasks: Arc::new(Mutex::new(JoinSet::new())), + packet_recv: Arc::new(Mutex::new(packet_recv)), + packet_sender, + + net: Arc::new(Mutex::new(None)), + entries: Arc::new(DashSet::new()), + }) + } + + async fn run_net_update_task(self: &Arc) { + let net = self.net.clone(); + let global_ctx = self.global_ctx.clone(); + let peer_manager = self.peer_manager.clone(); + let packet_recv = self.packet_recv.clone(); + let entries = self.entries.clone(); + self.tasks.lock().await.spawn(async move { + let mut prev_ipv4 = None; + loop { + let mut event_recv = global_ctx.subscribe(); + + let cur_ipv4 = global_ctx.get_ipv4(); + if prev_ipv4 != cur_ipv4 { + prev_ipv4 = cur_ipv4; + entries.clear(); + + if cur_ipv4.is_none() { + let _ = net.lock().await.take(); + } else { + net.lock().await.replace(Socks5ServerNet::new( + cur_ipv4.unwrap(), + None, + peer_manager.clone(), + packet_recv.clone(), + entries.clone(), + )); + } + } + + select! { + _ = event_recv.recv() => {} + _ = tokio::time::sleep(Duration::from_secs(120)) => {} + } + } + }); + } + + pub async fn run(self: &Arc) -> Result<(), Error> { + let Some(proxy_url) = self.global_ctx.config.get_socks5_portal() else { + return Ok(()); + }; + + let bind_addr = format!( + "{}:{}", + proxy_url.host_str().unwrap(), + proxy_url.port().unwrap() + ); + + let listener = { + let _g = self.global_ctx.net_ns.guard(); + TcpListener::bind(bind_addr.parse::().unwrap()).await? + }; + + self.peer_manager + .add_packet_process_pipeline(Box::new(self.clone())) + .await; + + self.run_net_update_task().await; + + let net = self.net.clone(); + self.tasks.lock().await.spawn(async move { + loop { + match listener.accept().await { + Ok((socket, _addr)) => { + tracing::info!("accept a new connection, {:?}", socket); + if let Some(net) = net.lock().await.as_ref() { + net.handle_tcp_stream(socket); + } + } + Err(err) => tracing::error!("accept error = {:?}", err), + } + } + }); + + Ok(()) + } +} diff --git a/rust/easytier/src/gateway/tcp_proxy.rs b/rust/easytier/src/gateway/tcp_proxy.rs new file mode 100644 index 0000000..05d617b --- /dev/null +++ b/rust/easytier/src/gateway/tcp_proxy.rs @@ -0,0 +1,917 @@ +use anyhow::Context; +use cidr::Ipv4Inet; +use core::panic; +use crossbeam::atomic::AtomicCell; +use dashmap::DashMap; +use pnet::packet::ip::IpNextHeaderProtocols; +use pnet::packet::ipv4::{Ipv4Packet, MutableIpv4Packet}; +use pnet::packet::tcp::{ipv4_checksum, MutableTcpPacket, TcpPacket}; +use pnet::packet::MutablePacket; +use pnet::packet::Packet; +use socket2::{SockRef, TcpKeepalive}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::sync::atomic::{AtomicBool, AtomicU16}; +use std::sync::{Arc, Weak}; +use std::time::{Duration, Instant}; +use tokio::io::{copy_bidirectional, AsyncRead, AsyncWrite, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpSocket, TcpStream}; +use tokio::select; +use tokio::sync::{mpsc, Mutex}; +use tokio::task::JoinSet; +use tokio::time::timeout; +use tracing::Instrument; + +use crate::common::error::Result; +use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtx}; +use crate::common::join_joinset_background; + +use crate::peers::peer_manager::PeerManager; +use crate::peers::{NicPacketFilter, PeerPacketFilter}; +use crate::proto::cli::{ + ListTcpProxyEntryRequest, ListTcpProxyEntryResponse, TcpProxyEntry, TcpProxyEntryState, + TcpProxyEntryTransportType, TcpProxyRpc, +}; +use crate::proto::rpc_types; +use crate::proto::rpc_types::controller::BaseController; +use crate::tunnel::packet_def::{PacketType, PeerManagerHeader, ZCPacket}; + +use super::CidrSet; + +#[cfg(feature = "smoltcp")] +use super::tokio_smoltcp::{self, channel_device, Net, NetConfig}; + +#[async_trait::async_trait] +pub(crate) trait NatDstConnector: Send + Sync + Clone + 'static { + type DstStream: AsyncRead + AsyncWrite + Unpin + Send; + + async fn connect(&self, src: SocketAddr, dst: SocketAddr) -> Result; + fn check_packet_from_peer_fast(&self, cidr_set: &CidrSet, global_ctx: &GlobalCtx) -> bool; + fn check_packet_from_peer( + &self, + cidr_set: &CidrSet, + global_ctx: &GlobalCtx, + hdr: &PeerManagerHeader, + ipv4: &Ipv4Packet, + ) -> bool; + fn transport_type(&self) -> TcpProxyEntryTransportType; +} + +#[derive(Debug, Clone)] +pub struct NatDstTcpConnector; + +#[async_trait::async_trait] +impl NatDstConnector for NatDstTcpConnector { + type DstStream = TcpStream; + async fn connect(&self, _src: SocketAddr, nat_dst: SocketAddr) -> Result { + let socket = TcpSocket::new_v4().unwrap(); + if let Err(e) = socket.set_nodelay(true) { + tracing::warn!("set_nodelay failed, ignore it: {:?}", e); + } + + const TCP_KEEPALIVE_TIME: Duration = Duration::from_secs(5); + const TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(2); + const TCP_KEEPALIVE_RETRIES: u32 = 2; + + let stream = timeout(Duration::from_secs(10), socket.connect(nat_dst)) + .await? + .with_context(|| format!("connect to nat dst failed: {:?}", nat_dst))?; + + let ka = TcpKeepalive::new() + .with_time(TCP_KEEPALIVE_TIME) + .with_interval(TCP_KEEPALIVE_INTERVAL); + + #[cfg(not(target_os = "windows"))] + let ka = ka.with_retries(TCP_KEEPALIVE_RETRIES); + + let sf = SockRef::from(&stream); + sf.set_tcp_keepalive(&ka)?; + + Ok(stream) + } + + fn check_packet_from_peer_fast(&self, cidr_set: &CidrSet, global_ctx: &GlobalCtx) -> bool { + !cidr_set.is_empty() || global_ctx.enable_exit_node() || global_ctx.no_tun() + } + + fn check_packet_from_peer( + &self, + cidr_set: &CidrSet, + global_ctx: &GlobalCtx, + hdr: &PeerManagerHeader, + ipv4: &Ipv4Packet, + ) -> bool { + let is_exit_node = hdr.is_exit_node(); + + if !cidr_set.contains_v4(ipv4.get_destination()) + && !is_exit_node + && !(global_ctx.no_tun() + && Some(ipv4.get_destination()) + == global_ctx.get_ipv4().as_ref().map(Ipv4Inet::address)) + { + return false; + } + + true + } + + fn transport_type(&self) -> TcpProxyEntryTransportType { + TcpProxyEntryTransportType::Tcp + } +} + +type NatDstEntryState = TcpProxyEntryState; + +#[derive(Debug)] +pub struct NatDstEntry { + id: uuid::Uuid, + src: SocketAddr, + dst: SocketAddr, + start_time: Instant, + start_time_local: chrono::DateTime, + tasks: Mutex>, + state: AtomicCell, +} + +impl NatDstEntry { + pub fn new(src: SocketAddr, dst: SocketAddr) -> Self { + Self { + id: uuid::Uuid::new_v4(), + src, + dst, + start_time: Instant::now(), + start_time_local: chrono::Local::now(), + tasks: Mutex::new(JoinSet::new()), + state: AtomicCell::new(NatDstEntryState::SynReceived), + } + } + + fn into_pb(&self, transport_type: TcpProxyEntryTransportType) -> TcpProxyEntry { + TcpProxyEntry { + src: Some(self.src.clone().into()), + dst: Some(self.dst.clone().into()), + start_time: self.start_time_local.timestamp() as u64, + state: self.state.load().into(), + transport_type: transport_type.into(), + } + } +} + +enum ProxyTcpStream { + KernelTcpStream(TcpStream), + #[cfg(feature = "smoltcp")] + SmolTcpStream(tokio_smoltcp::TcpStream), +} + +impl ProxyTcpStream { + pub fn set_nodelay(&self, nodelay: bool) -> Result<()> { + match self { + Self::KernelTcpStream(stream) => stream.set_nodelay(nodelay).map_err(Into::into), + #[cfg(feature = "smoltcp")] + Self::SmolTcpStream(_stream) => { + tracing::warn!("smol tcp stream set_nodelay not implemented"); + Ok(()) + } + } + } + + pub async fn shutdown(&mut self) -> Result<()> { + match self { + Self::KernelTcpStream(stream) => { + stream.shutdown().await?; + Ok(()) + } + #[cfg(feature = "smoltcp")] + Self::SmolTcpStream(stream) => { + stream.shutdown().await?; + Ok(()) + } + } + } + + pub async fn copy_bidirectional( + &mut self, + dst: &mut D, + ) -> Result<()> { + match self { + Self::KernelTcpStream(stream) => { + copy_bidirectional(stream, dst).await?; + Ok(()) + } + #[cfg(feature = "smoltcp")] + Self::SmolTcpStream(stream) => { + copy_bidirectional(stream, dst).await?; + Ok(()) + } + } + } +} + +#[cfg(feature = "smoltcp")] +struct SmolTcpListener { + listener_task: JoinSet<()>, + listen_count: usize, + + stream_rx: mpsc::UnboundedReceiver>, +} + +#[cfg(feature = "smoltcp")] +impl SmolTcpListener { + pub async fn new(net: Arc>>, listen_count: usize) -> Self { + let mut tasks = JoinSet::new(); + + let (tx, rx) = mpsc::unbounded_channel(); + let locked_net = net.lock().await; + for _ in 0..listen_count { + let mut tcp = locked_net + .as_ref() + .unwrap() + .tcp_bind("0.0.0.0:8899".parse().unwrap()) + .await + .unwrap(); + let tx = tx.clone(); + tasks.spawn(async move { + let mut not_listening_count = 0; + loop { + select! { + _ = tokio::time::sleep(Duration::from_secs(2)) => { + if tcp.is_listening() { + not_listening_count = 0; + continue; + } + + not_listening_count += 1; + if not_listening_count >= 2 { + tracing::error!("smol tcp listener not listening"); + tcp.relisten(); + } + } + accept_ret = tcp.accept() => { + tx.send(accept_ret.map_err(|e| { + anyhow::anyhow!("smol tcp listener accept failed: {:?}", e).into() + })) + .unwrap(); + not_listening_count = 0; + } + } + } + }); + } + + Self { + listener_task: tasks, + listen_count, + stream_rx: rx, + } + } + + pub async fn accept(&mut self) -> Result<(tokio_smoltcp::TcpStream, SocketAddr)> { + self.stream_rx.recv().await.unwrap() + } +} + +enum ProxyTcpListener { + KernelTcpListener(TcpListener), + #[cfg(feature = "smoltcp")] + SmolTcpListener(SmolTcpListener), +} + +impl ProxyTcpListener { + pub async fn accept(&mut self) -> Result<(ProxyTcpStream, SocketAddr)> { + match self { + Self::KernelTcpListener(listener) => { + let (stream, addr) = listener.accept().await?; + Ok((ProxyTcpStream::KernelTcpStream(stream), addr)) + } + #[cfg(feature = "smoltcp")] + Self::SmolTcpListener(listener) => { + let Ok((stream, src)) = listener.accept().await else { + return Err(anyhow::anyhow!("smol tcp listener closed").into()); + }; + tracing::info!(?src, "smol tcp listener accepted"); + Ok((ProxyTcpStream::SmolTcpStream(stream), src)) + } + } + } +} + +type ArcNatDstEntry = Arc; + +type SynSockMap = Arc>; +type ConnSockMap = Arc>; +// peer src addr to nat entry, when respond tcp packet, should modify the tcp src addr to the nat entry's dst addr +type AddrConnSockMap = Arc>; + +#[derive(Debug)] +pub struct TcpProxy { + global_ctx: Arc, + peer_manager: Arc, + local_port: AtomicU16, + + tasks: Arc>>, + + syn_map: SynSockMap, + conn_map: ConnSockMap, + addr_conn_map: AddrConnSockMap, + + cidr_set: CidrSet, + + smoltcp_stack_sender: Option>, + smoltcp_stack_receiver: Arc>>>, + #[cfg(feature = "smoltcp")] + smoltcp_net: Arc>>, + enable_smoltcp: Arc, + + connector: C, +} + +#[async_trait::async_trait] +impl PeerPacketFilter for TcpProxy { + async fn try_process_packet_from_peer(&self, mut packet: ZCPacket) -> Option { + if let Some(_) = self.try_handle_peer_packet(&mut packet).await { + if self + .enable_smoltcp + .load(std::sync::atomic::Ordering::Relaxed) + { + let smoltcp_stack_sender = self.smoltcp_stack_sender.as_ref().unwrap(); + if let Err(e) = smoltcp_stack_sender.try_send(packet) { + tracing::error!("send to smoltcp stack failed: {:?}", e); + } + } else { + if let Err(e) = self.peer_manager.get_nic_channel().send(packet).await { + tracing::error!("send to nic failed: {:?}", e); + } + } + return None; + } else { + Some(packet) + } + } +} + +#[async_trait::async_trait] +impl NicPacketFilter for TcpProxy { + async fn try_process_packet_from_nic(&self, zc_packet: &mut ZCPacket) -> bool { + let Some(my_ipv4) = self.get_local_ip() else { + return false; + }; + + let data = zc_packet.payload(); + let ip_packet = Ipv4Packet::new(data).unwrap(); + if ip_packet.get_version() != 4 + || ip_packet.get_source() != my_ipv4 + || ip_packet.get_next_level_protocol() != IpNextHeaderProtocols::Tcp + { + return false; + } + + let tcp_packet = TcpPacket::new(ip_packet.payload()).unwrap(); + if tcp_packet.get_source() != self.get_local_port() { + return false; + } + + let mut dst_addr = SocketAddr::V4(SocketAddrV4::new( + ip_packet.get_destination(), + tcp_packet.get_destination(), + )); + let mut need_transform_dst = false; + + // for kcp proxy, the src ip of nat entry will be converted from my ip to fake ip + // here we need to convert it back + if !self.is_smoltcp_enabled() && dst_addr.ip() == Self::get_fake_local_ipv4(my_ipv4) { + dst_addr.set_ip(IpAddr::V4(my_ipv4)); + need_transform_dst = true; + } + + tracing::trace!(dst_addr = ?dst_addr, "tcp packet try find entry"); + let entry = if let Some(entry) = self.addr_conn_map.get(&dst_addr) { + entry + } else { + let Some(syn_entry) = self.syn_map.get(&dst_addr) else { + return false; + }; + syn_entry + }; + let nat_entry = entry.clone(); + drop(entry); + assert_eq!(nat_entry.src, dst_addr); + + let IpAddr::V4(ip) = nat_entry.dst.ip() else { + panic!("v4 nat entry src ip is not v4"); + }; + + zc_packet + .mut_peer_manager_header() + .unwrap() + .set_no_proxy(true); + if need_transform_dst { + zc_packet.mut_peer_manager_header().unwrap().to_peer_id = self.get_my_peer_id().into(); + } + + let mut ip_packet = MutableIpv4Packet::new(zc_packet.mut_payload()).unwrap(); + ip_packet.set_source(ip); + if need_transform_dst { + ip_packet.set_destination(my_ipv4); + } + let dst = ip_packet.get_destination(); + + let mut tcp_packet = MutableTcpPacket::new(ip_packet.payload_mut()).unwrap(); + tcp_packet.set_source(nat_entry.dst.port()); + + Self::update_tcp_packet_checksum(&mut tcp_packet, &ip, &dst); + drop(tcp_packet); + Self::update_ip_packet_checksum(&mut ip_packet); + + tracing::trace!(dst_addr = ?dst_addr, nat_entry = ?nat_entry, packet = ?ip_packet, "tcp packet after modified"); + + true + } +} + +impl TcpProxy { + pub fn new(peer_manager: Arc, connector: C) -> Arc { + let (smoltcp_stack_sender, smoltcp_stack_receiver) = mpsc::channel::(1000); + let global_ctx = peer_manager.get_global_ctx(); + + Arc::new(Self { + global_ctx: global_ctx.clone(), + peer_manager, + + local_port: AtomicU16::new(0), + tasks: Arc::new(std::sync::Mutex::new(JoinSet::new())), + + syn_map: Arc::new(DashMap::new()), + conn_map: Arc::new(DashMap::new()), + addr_conn_map: Arc::new(DashMap::new()), + + cidr_set: CidrSet::new(global_ctx), + + smoltcp_stack_sender: Some(smoltcp_stack_sender), + smoltcp_stack_receiver: Arc::new(Mutex::new(Some(smoltcp_stack_receiver))), + + #[cfg(feature = "smoltcp")] + smoltcp_net: Arc::new(Mutex::new(None)), + + enable_smoltcp: Arc::new(AtomicBool::new(true)), + + connector, + }) + } + + fn update_tcp_packet_checksum( + tcp_packet: &mut MutableTcpPacket, + ipv4_src: &Ipv4Addr, + ipv4_dst: &Ipv4Addr, + ) { + tcp_packet.set_checksum(ipv4_checksum( + &tcp_packet.to_immutable(), + ipv4_src, + ipv4_dst, + )); + } + + fn update_ip_packet_checksum(ip_packet: &mut MutableIpv4Packet) { + ip_packet.set_checksum(pnet::packet::ipv4::checksum(&ip_packet.to_immutable())); + } + + pub async fn start(self: &Arc, add_pipeline: bool) -> Result<()> { + self.run_syn_map_cleaner().await?; + self.run_listener().await?; + if add_pipeline { + self.peer_manager + .add_packet_process_pipeline(Box::new(self.clone())) + .await; + self.peer_manager + .add_nic_packet_process_pipeline(Box::new(self.clone())) + .await; + } + join_joinset_background(self.tasks.clone(), "TcpProxy".to_owned()); + + Ok(()) + } + + async fn run_syn_map_cleaner(&self) -> Result<()> { + let syn_map = self.syn_map.clone(); + let tasks = self.tasks.clone(); + let syn_map_cleaner_task = async move { + loop { + syn_map.retain(|_, entry| { + if entry.start_time.elapsed() > Duration::from_secs(30) { + tracing::warn!(entry = ?entry, "syn nat entry expired"); + entry.state.store(NatDstEntryState::Closed); + false + } else { + true + } + }); + tokio::time::sleep(Duration::from_secs(10)).await; + } + }; + tasks.lock().unwrap().spawn(syn_map_cleaner_task); + + Ok(()) + } + + async fn get_proxy_listener(&self) -> Result { + #[cfg(feature = "smoltcp")] + if self.global_ctx.get_flags().use_smoltcp + || self.global_ctx.no_tun() + || cfg!(target_os = "android") + { + // use smoltcp network stack + self.local_port + .store(8899, std::sync::atomic::Ordering::Relaxed); + + let mut cap = smoltcp::phy::DeviceCapabilities::default(); + cap.max_transmission_unit = 1280; + cap.medium = smoltcp::phy::Medium::Ip; + let (dev, stack_sink, mut stack_stream) = channel_device::ChannelDevice::new(cap); + + let mut smoltcp_stack_receiver = + self.smoltcp_stack_receiver.lock().await.take().unwrap(); + self.tasks.lock().unwrap().spawn(async move { + while let Some(packet) = smoltcp_stack_receiver.recv().await { + tracing::trace!(?packet, "receive from peer send to smoltcp packet"); + if let Err(e) = stack_sink.send(Ok(packet.payload().to_vec())).await { + tracing::error!("send to smoltcp stack failed: {:?}", e); + } + } + tracing::error!("smoltcp stack sink exited"); + panic!("smoltcp stack sink exited"); + }); + + let peer_mgr = self.peer_manager.clone(); + self.tasks.lock().unwrap().spawn(async move { + while let Some(data) = stack_stream.recv().await { + tracing::trace!( + ?data, + "receive from smoltcp stack and send to peer mgr packet" + ); + let Some(ipv4) = Ipv4Packet::new(&data) else { + tracing::error!(?data, "smoltcp stack stream get non ipv4 packet"); + continue; + }; + + let dst = ipv4.get_destination(); + let packet = ZCPacket::new_with_payload(&data); + if let Err(e) = peer_mgr.send_msg_ipv4(packet, dst).await { + tracing::error!("send to peer failed in smoltcp sender: {:?}", e); + } + } + tracing::error!("smoltcp stack stream exited"); + panic!("smoltcp stack stream exited"); + }); + + let interface_config = smoltcp::iface::Config::new(smoltcp::wire::HardwareAddress::Ip); + let net = Net::new( + dev, + NetConfig::new( + interface_config, + format!("{}/24", self.get_local_ip().unwrap()) + .parse() + .unwrap(), + vec![format!("{}", self.get_local_ip().unwrap()).parse().unwrap()], + ), + ); + net.set_any_ip(true); + self.smoltcp_net.lock().await.replace(net); + let tcp = SmolTcpListener::new(self.smoltcp_net.clone(), 64).await; + + self.enable_smoltcp + .store(true, std::sync::atomic::Ordering::Relaxed); + + return Ok(ProxyTcpListener::SmolTcpListener(tcp)); + } + + { + // use kernel network stack + let listen_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0); + let net_ns = self.global_ctx.net_ns.clone(); + let tcp_listener = net_ns + .run_async(|| async { TcpListener::bind(&listen_addr).await }) + .await?; + self.local_port.store( + tcp_listener.local_addr()?.port(), + std::sync::atomic::Ordering::Relaxed, + ); + + self.enable_smoltcp + .store(false, std::sync::atomic::Ordering::Relaxed); + + return Ok(ProxyTcpListener::KernelTcpListener(tcp_listener)); + } + } + + async fn run_listener(&self) -> Result<()> { + // bind on both v4 & v6 + let mut tcp_listener = self.get_proxy_listener().await?; + + let global_ctx = self.global_ctx.clone(); + let tasks = self.tasks.clone(); + let syn_map = self.syn_map.clone(); + let conn_map = self.conn_map.clone(); + let addr_conn_map = self.addr_conn_map.clone(); + let connector = self.connector.clone(); + let accept_task = async move { + let conn_map = conn_map.clone(); + loop { + let accept_ret = tcp_listener.accept().await; + let Ok((tcp_stream, mut socket_addr)) = accept_ret else { + tracing::error!("nat tcp listener accept failed: {:?}", accept_ret.err()); + continue; + }; + + let my_ip = global_ctx + .get_ipv4() + .as_ref() + .map(Ipv4Inet::address) + .unwrap_or(Ipv4Addr::UNSPECIFIED); + + if socket_addr.ip() == Self::get_fake_local_ipv4(my_ip) { + socket_addr.set_ip(IpAddr::V4(my_ip)); + } + + let Some(entry) = syn_map.get(&socket_addr) else { + tracing::error!( + ?my_ip, + ?socket_addr, + "tcp connection from unknown source, ignore it" + ); + continue; + }; + tracing::info!( + ?socket_addr, + "tcp connection accepted for proxy, nat dst: {:?}", + entry.dst + ); + assert_eq!(entry.state.load(), NatDstEntryState::SynReceived); + + let entry_clone = entry.clone(); + drop(entry); + syn_map.remove_if(&socket_addr, |_, entry| entry.id == entry_clone.id); + + entry_clone.state.store(NatDstEntryState::ConnectingDst); + + let _ = addr_conn_map.insert(entry_clone.src, entry_clone.clone()); + let old_nat_val = conn_map.insert(entry_clone.id, entry_clone.clone()); + assert!(old_nat_val.is_none()); + + tasks.lock().unwrap().spawn(Self::connect_to_nat_dst( + connector.clone(), + global_ctx.clone(), + tcp_stream, + conn_map.clone(), + addr_conn_map.clone(), + entry_clone, + )); + } + }; + self.tasks + .lock() + .unwrap() + .spawn(accept_task.instrument(tracing::info_span!("tcp_proxy_listener"))); + + Ok(()) + } + + fn remove_entry_from_all_conn_map( + conn_map: ConnSockMap, + addr_conn_map: AddrConnSockMap, + nat_entry: ArcNatDstEntry, + ) { + conn_map.remove(&nat_entry.id); + addr_conn_map.remove_if(&nat_entry.src, |_, entry| entry.id == nat_entry.id); + } + + async fn connect_to_nat_dst( + connector: C, + global_ctx: ArcGlobalCtx, + src_tcp_stream: ProxyTcpStream, + conn_map: ConnSockMap, + addr_conn_map: AddrConnSockMap, + nat_entry: ArcNatDstEntry, + ) { + if let Err(e) = src_tcp_stream.set_nodelay(true) { + tracing::warn!("set_nodelay failed, ignore it: {:?}", e); + } + + let nat_dst = if Some(nat_entry.dst.ip()) + == global_ctx.get_ipv4().map(|ip| IpAddr::V4(ip.address())) + { + format!("127.0.0.1:{}", nat_entry.dst.port()) + .parse() + .unwrap() + } else { + nat_entry.dst + }; + + let _guard = global_ctx.net_ns.guard(); + let Ok(dst_tcp_stream) = connector.connect(nat_entry.src, nat_dst).await else { + tracing::error!("connect to dst failed: {:?}", nat_entry); + nat_entry.state.store(NatDstEntryState::Closed); + Self::remove_entry_from_all_conn_map(conn_map, addr_conn_map, nat_entry); + return; + }; + drop(_guard); + + tracing::info!(?nat_entry, ?nat_dst, "tcp connection to dst established"); + + assert_eq!(nat_entry.state.load(), NatDstEntryState::ConnectingDst); + nat_entry.state.store(NatDstEntryState::Connected); + + Self::handle_nat_connection( + src_tcp_stream, + dst_tcp_stream, + conn_map, + addr_conn_map, + nat_entry, + ) + .await; + } + + async fn handle_nat_connection( + mut src_tcp_stream: ProxyTcpStream, + mut dst_tcp_stream: C::DstStream, + conn_map: ConnSockMap, + addr_conn_map: AddrConnSockMap, + nat_entry: ArcNatDstEntry, + ) { + let nat_entry_clone = nat_entry.clone(); + nat_entry.tasks.lock().await.spawn(async move { + let ret = src_tcp_stream.copy_bidirectional(&mut dst_tcp_stream).await; + tracing::info!(nat_entry = ?nat_entry_clone, ret = ?ret, "nat tcp connection closed"); + + nat_entry_clone.state.store(NatDstEntryState::ClosingSrc); + let ret = timeout(Duration::from_secs(10), src_tcp_stream.shutdown()).await; + tracing::info!(nat_entry = ?nat_entry_clone, ret = ?ret, "src tcp stream shutdown"); + + nat_entry_clone.state.store(NatDstEntryState::ClosingDst); + let ret = timeout(Duration::from_secs(10), dst_tcp_stream.shutdown()).await; + tracing::info!(nat_entry = ?nat_entry_clone, ret = ?ret, "dst tcp stream shutdown"); + + drop(src_tcp_stream); + drop(dst_tcp_stream); + + nat_entry_clone.state.store(NatDstEntryState::Closed); + // sleep later so the fin packet can be processed + tokio::time::sleep(Duration::from_secs(10)).await; + + Self::remove_entry_from_all_conn_map(conn_map, addr_conn_map, nat_entry_clone); + }); + } + + pub fn get_local_port(&self) -> u16 { + self.local_port.load(std::sync::atomic::Ordering::Relaxed) + } + + pub fn get_my_peer_id(&self) -> u32 { + self.peer_manager.my_peer_id() + } + + pub fn get_local_ip(&self) -> Option { + if self.is_smoltcp_enabled() { + Some(Ipv4Addr::new(192, 88, 99, 254)) + } else { + self.global_ctx + .get_ipv4() + .as_ref() + .map(cidr::Ipv4Inet::address) + } + } + + pub fn get_global_ctx(&self) -> &ArcGlobalCtx { + &self.global_ctx + } + + pub fn is_smoltcp_enabled(&self) -> bool { + self.enable_smoltcp + .load(std::sync::atomic::Ordering::Relaxed) + } + + pub fn get_fake_local_ipv4(local_ip: Ipv4Addr) -> Ipv4Addr { + let octets = local_ip.octets(); + Ipv4Addr::new(octets[0], octets[1], octets[2], 0) + } + + async fn try_handle_peer_packet(&self, packet: &mut ZCPacket) -> Option<()> { + if !self + .connector + .check_packet_from_peer_fast(&self.cidr_set, &self.global_ctx) + { + return None; + } + + let ipv4_addr = self.get_local_ip()?; + let hdr = packet.peer_manager_header().unwrap().clone(); + + if hdr.packet_type != PacketType::Data as u8 || hdr.is_no_proxy() { + return None; + }; + + let payload_bytes = packet.mut_payload(); + + let ipv4 = Ipv4Packet::new(payload_bytes)?; + if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Tcp { + return None; + } + + if !self + .connector + .check_packet_from_peer(&self.cidr_set, &self.global_ctx, &hdr, &ipv4) + { + return None; + } + + tracing::trace!(ipv4 = ?ipv4, cidr_set = ?self.cidr_set, "proxy tcp packet received"); + + let ip_packet = Ipv4Packet::new(payload_bytes).unwrap(); + let tcp_packet = TcpPacket::new(ip_packet.payload()).unwrap(); + + let source_ip = ip_packet.get_source(); + let source_port = tcp_packet.get_source(); + let src = SocketAddr::V4(SocketAddrV4::new(source_ip, source_port)); + + let is_tcp_syn = tcp_packet.get_flags() & pnet::packet::tcp::TcpFlags::SYN != 0; + let is_tcp_ack = tcp_packet.get_flags() & pnet::packet::tcp::TcpFlags::ACK != 0; + if is_tcp_syn && !is_tcp_ack { + let dest_ip = ip_packet.get_destination(); + let dest_port = tcp_packet.get_destination(); + let dst = SocketAddr::V4(SocketAddrV4::new(dest_ip, dest_port)); + + let old_val = self + .syn_map + .insert(src, Arc::new(NatDstEntry::new(src, dst))); + tracing::info!(src = ?src, dst = ?dst, old_entry = ?old_val, "tcp syn received"); + } else if !self.addr_conn_map.contains_key(&src) && !self.syn_map.contains_key(&src) { + // if not in syn map and addr conn map, may forwarding n2n packet + return None; + } + + let mut ip_packet = MutableIpv4Packet::new(payload_bytes).unwrap(); + if !self.is_smoltcp_enabled() && source_ip == ipv4_addr { + // modify the source so the response packet can be handled by tun device + ip_packet.set_source(Self::get_fake_local_ipv4(ipv4_addr)); + } + ip_packet.set_destination(ipv4_addr); + let source = ip_packet.get_source(); + + let mut tcp_packet = MutableTcpPacket::new(ip_packet.payload_mut()).unwrap(); + tcp_packet.set_destination(self.get_local_port()); + + Self::update_tcp_packet_checksum(&mut tcp_packet, &source, &ipv4_addr); + drop(tcp_packet); + Self::update_ip_packet_checksum(&mut ip_packet); + + tracing::trace!(?source, ?ipv4_addr, ?packet, "tcp packet after modified"); + + Some(()) + } + + pub fn get_peer_manager(&self) -> &Arc { + &self.peer_manager + } + + pub fn is_tcp_proxy_connection(&self, src: SocketAddr) -> bool { + self.syn_map.contains_key(&src) || self.addr_conn_map.contains_key(&src) + } + + pub fn list_proxy_entries(&self) -> Vec { + let mut entries: Vec = Vec::new(); + let transport_type = self.connector.transport_type(); + for entry in self.syn_map.iter() { + entries.push(entry.value().as_ref().into_pb(transport_type)); + } + for entry in self.conn_map.iter() { + entries.push(entry.value().as_ref().into_pb(transport_type)); + } + entries + } +} + +#[derive(Clone)] +pub struct TcpProxyRpcService { + tcp_proxy: Weak>, +} + +#[async_trait::async_trait] +impl TcpProxyRpc for TcpProxyRpcService { + type Controller = BaseController; + async fn list_tcp_proxy_entry( + &self, + _: BaseController, + _request: ListTcpProxyEntryRequest, // Accept request of type HelloRequest + ) -> std::result::Result { + let mut reply = ListTcpProxyEntryResponse::default(); + if let Some(tcp_proxy) = self.tcp_proxy.upgrade() { + reply.entries = tcp_proxy.list_proxy_entries(); + } + Ok(reply) + } +} + +impl TcpProxyRpcService { + pub fn new(tcp_proxy: Arc>) -> Self { + Self { + tcp_proxy: Arc::downgrade(&tcp_proxy), + } + } +} diff --git a/rust/easytier/src/gateway/tokio_smoltcp/channel_device.rs b/rust/easytier/src/gateway/tokio_smoltcp/channel_device.rs new file mode 100644 index 0000000..6ee90d4 --- /dev/null +++ b/rust/easytier/src/gateway/tokio_smoltcp/channel_device.rs @@ -0,0 +1,75 @@ +use futures::{Sink, Stream}; +use smoltcp::phy::DeviceCapabilities; +use std::{ + io, + pin::Pin, + task::{Context, Poll}, +}; +use tokio::sync::mpsc::{channel, Receiver, Sender}; +use tokio_util::sync::{PollSendError, PollSender}; + +use super::device::AsyncDevice; + +/// A device that send and receive packets using a channel. +pub struct ChannelDevice { + recv: Receiver>>, + send: PollSender>, + caps: DeviceCapabilities, +} + +impl ChannelDevice { + /// Make a new `ChannelDevice` with the given `recv` and `send` channels. + /// + /// The `caps` is used to determine the device capabilities. `DeviceCapabilities::max_transmission_unit` must be set. + pub fn new(caps: DeviceCapabilities) -> (Self, Sender>>, Receiver>) { + let (tx1, rx1) = channel(1000); + let (tx2, rx2) = channel(1000); + ( + ChannelDevice { + send: PollSender::new(tx1), + recv: rx2, + caps, + }, + tx2, + rx1, + ) + } +} + +impl Stream for ChannelDevice { + type Item = io::Result>; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.recv.poll_recv(cx) + } +} + +fn map_err(e: PollSendError>) -> io::Error { + io::Error::new(io::ErrorKind::Other, e) +} + +impl Sink> for ChannelDevice { + type Error = io::Error; + + fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.send.poll_reserve(cx).map_err(map_err) + } + + fn start_send(mut self: Pin<&mut Self>, item: Vec) -> Result<(), Self::Error> { + self.send.send_item(item).map_err(map_err) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.send.poll_reserve(cx).map_err(map_err) + } + + fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl AsyncDevice for ChannelDevice { + fn capabilities(&self) -> &DeviceCapabilities { + &self.caps + } +} diff --git a/rust/easytier/src/gateway/tokio_smoltcp/device.rs b/rust/easytier/src/gateway/tokio_smoltcp/device.rs new file mode 100644 index 0000000..3bbe54b --- /dev/null +++ b/rust/easytier/src/gateway/tokio_smoltcp/device.rs @@ -0,0 +1,126 @@ +use futures::{Sink, Stream}; +pub use smoltcp::phy::DeviceCapabilities; +use smoltcp::{ + phy::{Device, RxToken, TxToken}, + time::Instant, +}; +use std::{collections::VecDeque, io}; + +/// Default value of `max_burst_size`. +pub const DEFAULT_MAX_BURST_SIZE: usize = 100; + +/// A packet used in `AsyncDevice`. +pub type Packet = Vec; + +/// A device that send and receive packets asynchronously. +pub trait AsyncDevice: + Stream> + Sink + Send + Unpin +{ + /// Returns the device capabilities. + fn capabilities(&self) -> &DeviceCapabilities; +} + +impl AsyncDevice for Box +where + T: AsyncDevice, +{ + fn capabilities(&self) -> &DeviceCapabilities { + (**self).capabilities() + } +} + +/// A device that send and receive packets synchronously. +pub struct BufferDevice { + caps: DeviceCapabilities, + max_burst_size: usize, + recv_queue: VecDeque, + send_queue: VecDeque, +} + +/// RxToken for `BufferDevice`. +pub struct BufferRxToken(Packet); + +impl RxToken for BufferRxToken { + fn consume(mut self, f: F) -> R + where + F: FnOnce(&[u8]) -> R, + { + let p = &mut self.0; + let result = f(p); + result + } +} + +/// TxToken for `BufferDevice`. +pub struct BufferTxToken<'a>(&'a mut BufferDevice); + +impl<'d> TxToken for BufferTxToken<'d> { + fn consume(self, len: usize, f: F) -> R + where + F: FnOnce(&mut [u8]) -> R, + { + let mut buffer = vec![0u8; len]; + let result = f(&mut buffer); + + self.0.send_queue.push_back(buffer); + + result + } +} + +impl Device for BufferDevice { + type RxToken<'a> + = BufferRxToken + where + Self: 'a; + type TxToken<'a> + = BufferTxToken<'a> + where + Self: 'a; + + fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { + match self.recv_queue.pop_front() { + Some(p) => Some((BufferRxToken(p), BufferTxToken(self))), + None => None, + } + } + + fn transmit(&mut self, _timestamp: Instant) -> Option> { + if self.send_queue.len() < self.max_burst_size { + Some(BufferTxToken(self)) + } else { + None + } + } + + fn capabilities(&self) -> DeviceCapabilities { + self.caps.clone() + } +} + +impl BufferDevice { + pub(crate) fn new(caps: DeviceCapabilities) -> BufferDevice { + let max_burst_size = caps.max_burst_size.unwrap_or(DEFAULT_MAX_BURST_SIZE); + BufferDevice { + caps, + max_burst_size, + recv_queue: VecDeque::with_capacity(max_burst_size), + send_queue: VecDeque::with_capacity(max_burst_size), + } + } + pub(crate) fn take_send_queue(&mut self) -> VecDeque { + std::mem::replace( + &mut self.send_queue, + VecDeque::with_capacity(self.max_burst_size), + ) + } + pub(crate) fn push_recv_queue(&mut self, p: impl Iterator) { + self.recv_queue.extend(p.take(self.avaliable_recv_queue())); + } + pub(crate) fn avaliable_recv_queue(&self) -> usize { + self.max_burst_size - self.recv_queue.len() + } + pub(crate) fn need_wait(&self) -> bool { + self.recv_queue.is_empty() + } +} diff --git a/rust/easytier/src/gateway/tokio_smoltcp/mod.rs b/rust/easytier/src/gateway/tokio_smoltcp/mod.rs new file mode 100644 index 0000000..58805d0 --- /dev/null +++ b/rust/easytier/src/gateway/tokio_smoltcp/mod.rs @@ -0,0 +1,210 @@ +// most code is copied from https://github.com/spacemeowx2/tokio-smoltcp + +//! An asynchronous wrapper for smoltcp. + +use std::{ + io, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + sync::{ + atomic::{AtomicU16, Ordering}, + Arc, + }, +}; + +use device::BufferDevice; +use futures::Future; +use reactor::Reactor; +pub use smoltcp; +use smoltcp::{ + iface::{Config, Interface, Routes}, + time::{Duration, Instant}, + wire::{HardwareAddress, IpAddress, IpCidr}, +}; +pub use socket::{TcpListener, TcpStream}; +pub use socket_allocator::BufferSize; +use tokio::sync::Notify; + +/// The async devices. +pub mod channel_device; +pub mod device; +mod reactor; +mod socket; +mod socket_allocator; + +/// Can be used to create a forever timestamp in neighbor. +// The 60_000 is the same as NeighborCache::ENTRY_LIFETIME. +pub const FOREVER: Instant = + Instant::from_micros_const(i64::max_value() - Duration::from_millis(60_000).micros() as i64); + +pub struct Neighbor { + pub protocol_addr: IpAddress, + pub hardware_addr: HardwareAddress, + pub timestamp: Instant, +} + +/// A config for a `Net`. +/// +/// This is used to configure the `Net`. +#[non_exhaustive] +pub struct NetConfig { + pub interface_config: Config, + pub ip_addr: IpCidr, + pub gateway: Vec, + pub buffer_size: BufferSize, +} + +impl NetConfig { + pub fn new(interface_config: Config, ip_addr: IpCidr, gateway: Vec) -> Self { + Self { + interface_config, + ip_addr, + gateway, + buffer_size: Default::default(), + } + } +} + +/// `Net` is the main interface to the network stack. +/// Socket creation and configuration is done through the `Net` interface. +/// +/// When `Net` is dropped, all sockets are closed and the network stack is stopped. +pub struct Net { + reactor: Arc, + ip_addr: IpCidr, + from_port: AtomicU16, + stopper: Arc, +} + +impl std::fmt::Debug for Net { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Net") + .field("ip_addr", &self.ip_addr) + .field("from_port", &self.from_port) + .finish() + } +} + +impl Net { + /// Creates a new `Net` instance. It panics if the medium is not supported. + pub fn new(device: D, config: NetConfig) -> Net { + let (net, fut) = Self::new2(device, config); + tokio::spawn(fut); + net + } + + fn new2( + device: D, + config: NetConfig, + ) -> (Net, impl Future> + Send) { + let mut buffer_device = BufferDevice::new(device.capabilities().clone()); + let mut iface = Interface::new(config.interface_config, &mut buffer_device, Instant::now()); + let ip_addr = config.ip_addr; + iface.update_ip_addrs(|ip_addrs| { + ip_addrs.push(ip_addr).unwrap(); + }); + for gateway in config.gateway { + match gateway { + IpAddress::Ipv4(v4) => { + iface.routes_mut().add_default_ipv4_route(v4).unwrap(); + } + IpAddress::Ipv6(v6) => { + iface.routes_mut().add_default_ipv6_route(v6).unwrap(); + } + #[allow(unreachable_patterns)] + _ => panic!("Unsupported address"), + }; + } + + let stopper = Arc::new(Notify::new()); + let (reactor, fut) = Reactor::new( + device, + iface, + buffer_device, + config.buffer_size, + stopper.clone(), + ); + + ( + Net { + reactor: Arc::new(reactor), + ip_addr: config.ip_addr, + from_port: AtomicU16::new(10001), + stopper, + }, + fut, + ) + } + pub fn get_address(&self) -> IpAddr { + self.ip_addr.address().into() + } + pub fn get_port(&self) -> u16 { + self.from_port + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |x| { + Some(if x > 60000 { 10000 } else { x + 1 }) + }) + .unwrap() + } + /// Creates a new TcpListener, which will be bound to the specified address. + pub async fn tcp_bind(&self, addr: SocketAddr) -> io::Result { + let addr = self.set_address(addr); + TcpListener::new(self.reactor.clone(), addr.into()).await + } + /// Opens a TCP connection to a remote host. + pub async fn tcp_connect(&self, addr: SocketAddr, local_port: u16) -> io::Result { + TcpStream::connect( + self.reactor.clone(), + (self.ip_addr.address(), local_port).into(), + addr.into(), + ) + .await + } + fn set_address(&self, mut addr: SocketAddr) -> SocketAddr { + if addr.ip().is_unspecified() { + addr.set_ip(match self.ip_addr.address() { + IpAddress::Ipv4(ip) => Ipv4Addr::from(ip).into(), + IpAddress::Ipv6(ip) => Ipv6Addr::from(ip).into(), + #[allow(unreachable_patterns)] + _ => panic!("address must not be unspecified"), + }); + } + if addr.port() == 0 { + addr.set_port(self.get_port()); + } + addr + } + + /// Enable or disable the AnyIP capability. + pub fn set_any_ip(&self, any_ip: bool) { + let iface = self.reactor.iface().clone(); + let mut iface: parking_lot::lock_api::MutexGuard<'_, parking_lot::RawMutex, Interface> = + iface.lock(); + iface.set_any_ip(any_ip); + } + + /// Get whether AnyIP is enabled. + pub fn any_ip(&self) -> bool { + let iface = self.reactor.iface().clone(); + let iface = iface.lock(); + iface.any_ip() + } + + pub fn routes(&self, f: F) { + let iface = self.reactor.iface().clone(); + let iface = iface.lock(); + let routes = iface.routes(); + f(routes) + } + + pub fn routes_mut(&self, f: F) { + let iface = self.reactor.iface().clone(); + let mut iface = iface.lock(); + let routes = iface.routes_mut(); + f(routes) + } +} + +impl Drop for Net { + fn drop(&mut self) { + self.stopper.notify_waiters() + } +} diff --git a/rust/easytier/src/gateway/tokio_smoltcp/reactor.rs b/rust/easytier/src/gateway/tokio_smoltcp/reactor.rs new file mode 100644 index 0000000..b41c492 --- /dev/null +++ b/rust/easytier/src/gateway/tokio_smoltcp/reactor.rs @@ -0,0 +1,174 @@ +use super::{ + device::{BufferDevice, Packet}, + socket_allocator::{BufferSize, SocketAlloctor}, +}; +use futures::{stream::iter, FutureExt, SinkExt, StreamExt}; +use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; +use smoltcp::{ + iface::{Context, Interface, SocketHandle}, + socket::{AnySocket, Socket}, + time::{Duration, Instant}, +}; +use std::{collections::VecDeque, future::Future, io, sync::Arc}; +use tokio::{pin, select, sync::Notify, time::sleep}; + +pub(crate) type BufferInterface = Arc>; +const MAX_BURST_SIZE: usize = 100; + +pub(crate) struct Reactor { + notify: Arc, + iface: BufferInterface, + socket_allocator: SocketAlloctor, +} + +async fn receive( + async_iface: &mut impl super::device::AsyncDevice, + recv_buf: &mut VecDeque, +) -> io::Result<()> { + if let Some(packet) = async_iface.next().await { + recv_buf.push_back(packet?); + } + Ok(()) +} + +async fn run( + mut async_iface: impl super::device::AsyncDevice, + iface: BufferInterface, + mut device: BufferDevice, + socket_allocator: SocketAlloctor, + notify: Arc, + stopper: Arc, +) -> io::Result<()> { + let default_timeout = Duration::from_secs(60); + let timer = sleep(default_timeout.into()); + let max_burst_size = async_iface + .capabilities() + .max_burst_size + .unwrap_or(MAX_BURST_SIZE); + let mut recv_buf = VecDeque::with_capacity(max_burst_size); + pin!(timer); + + loop { + let packets = device.take_send_queue(); + + async_iface + .send_all(&mut iter(packets).map(|p| Ok(p))) + .await?; + + if recv_buf.is_empty() && device.need_wait() { + let start = Instant::now(); + let deadline = { + iface + .lock() + .poll_delay(start, &socket_allocator.sockets().lock()) + .unwrap_or(default_timeout) + }; + + timer + .as_mut() + .reset(tokio::time::Instant::now() + deadline.into()); + select! { + _ = &mut timer => {}, + _ = receive(&mut async_iface,&mut recv_buf) => {} + _ = notify.notified() => {} + _ = stopper.notified() => break, + }; + + while let (true, Some(Ok(p))) = ( + recv_buf.len() < max_burst_size, + async_iface.next().now_or_never().flatten(), + ) { + recv_buf.push_back(p); + } + } + + let mut iface = iface.lock(); + + device.push_recv_queue(recv_buf.drain(..device.avaliable_recv_queue().min(recv_buf.len()))); + + iface.poll( + Instant::now(), + &mut device, + &mut socket_allocator.sockets().lock(), + ); + + // wake up all closed sockets (smoltcp seems have a bug that it doesn't wake up closed sockets) + for (_, socket) in socket_allocator.sockets().lock().iter_mut() { + match socket { + Socket::Tcp(tcp) => { + if tcp.state() == smoltcp::socket::tcp::State::Closed { + tcp.abort(); + } + } + #[allow(unreachable_patterns)] + _ => {} + } + } + } + + Ok(()) +} + +impl Reactor { + pub fn new( + async_device: impl super::device::AsyncDevice, + iface: Interface, + device: BufferDevice, + buffer_size: BufferSize, + stopper: Arc, + ) -> (Self, impl Future> + Send) { + let iface = Arc::new(Mutex::new(iface)); + let notify = Arc::new(Notify::new()); + let socket_allocator = SocketAlloctor::new(buffer_size); + let fut = run( + async_device, + iface.clone(), + device, + socket_allocator.clone(), + notify.clone(), + stopper, + ); + + ( + Reactor { + notify, + iface: iface.clone(), + socket_allocator, + }, + fut, + ) + } + pub fn get_socket>( + &self, + handle: SocketHandle, + ) -> MappedMutexGuard<'_, T> { + MutexGuard::map( + self.socket_allocator.sockets().lock(), + |sockets: &mut smoltcp::iface::SocketSet<'_>| sockets.get_mut::(handle), + ) + } + pub fn context(&self) -> MappedMutexGuard<'_, Context> { + MutexGuard::map(self.iface.lock(), |iface| iface.context()) + } + pub fn socket_allocator(&self) -> &SocketAlloctor { + &self.socket_allocator + } + pub fn notify(&self) { + self.notify.notify_waiters(); + } + pub fn iface(&self) -> &BufferInterface { + &self.iface + } +} + +impl Drop for Reactor { + fn drop(&mut self) { + for (_, socket) in self.socket_allocator.sockets().lock().iter_mut() { + match socket { + Socket::Tcp(tcp) => tcp.close(), + #[allow(unreachable_patterns)] + _ => {} + } + } + } +} diff --git a/rust/easytier/src/gateway/tokio_smoltcp/socket.rs b/rust/easytier/src/gateway/tokio_smoltcp/socket.rs new file mode 100644 index 0000000..f46f80f --- /dev/null +++ b/rust/easytier/src/gateway/tokio_smoltcp/socket.rs @@ -0,0 +1,249 @@ +use super::{reactor::Reactor, socket_allocator::SocketHandle}; +use futures::future::{self, poll_fn}; +use futures::{ready, Stream}; +pub use smoltcp::socket::tcp; +use smoltcp::wire::{IpAddress, IpEndpoint}; +use std::mem::replace; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::{ + io, + net::SocketAddr, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +/// A TCP socket server, listening for connections. +/// +/// You can accept a new connection by using the accept method. +pub struct TcpListener { + handle: SocketHandle, + reactor: Arc, + local_addr: SocketAddr, +} + +fn map_err(e: E) -> io::Error { + io::Error::new(io::ErrorKind::Other, e.to_string()) +} + +impl TcpListener { + pub(super) async fn new( + reactor: Arc, + local_endpoint: IpEndpoint, + ) -> io::Result { + let handle = reactor.socket_allocator().new_tcp_socket(); + { + let mut socket = reactor.get_socket::(*handle); + socket.listen(local_endpoint).map_err(map_err)?; + } + + let local_addr = ep2sa(&local_endpoint); + Ok(TcpListener { + handle, + reactor, + local_addr, + }) + } + pub fn poll_accept( + &mut self, + cx: &mut Context<'_>, + ) -> Poll> { + let mut socket = self.reactor.get_socket::(*self.handle); + + if socket.state() == tcp::State::Established { + drop(socket); + return Poll::Ready(Ok(TcpStream::accept(self)?)); + } + socket.register_send_waker(cx.waker()); + Poll::Pending + } + pub async fn accept(&mut self) -> io::Result<(TcpStream, SocketAddr)> { + poll_fn(|cx| self.poll_accept(cx)).await + } + pub fn incoming(self) -> Incoming { + Incoming(self) + } + pub fn local_addr(&self) -> io::Result { + Ok(self.local_addr) + } + + pub fn relisten(&mut self) { + let mut socket = self.reactor.get_socket::(*self.handle); + let local_endpoint = socket.local_endpoint().unwrap(); + socket.abort(); + socket.listen(local_endpoint).unwrap(); + self.reactor.notify(); + } + + pub fn is_listening(&self) -> bool { + let socket = self.reactor.get_socket::(*self.handle); + socket.is_listening() + } +} + +pub struct Incoming(TcpListener); + +impl Stream for Incoming { + type Item = io::Result; + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let (tcp, _) = ready!(self.0.poll_accept(cx))?; + Poll::Ready(Some(Ok(tcp))) + } +} + +fn ep2sa(ep: &IpEndpoint) -> SocketAddr { + match ep.addr { + IpAddress::Ipv4(v4) => SocketAddr::new(IpAddr::V4(Ipv4Addr::from(v4)), ep.port), + IpAddress::Ipv6(v6) => SocketAddr::new(IpAddr::V6(Ipv6Addr::from(v6)), ep.port), + #[allow(unreachable_patterns)] + _ => unreachable!(), + } +} + +/// A TCP stream between a local and a remote socket. +pub struct TcpStream { + handle: SocketHandle, + reactor: Arc, + local_addr: SocketAddr, + peer_addr: SocketAddr, +} + +impl TcpStream { + pub(super) async fn connect( + reactor: Arc, + local_endpoint: IpEndpoint, + remote_endpoint: IpEndpoint, + ) -> io::Result { + let handle = reactor.socket_allocator().new_tcp_socket(); + + reactor + .get_socket::(*handle) + .connect(&mut reactor.context(), remote_endpoint, local_endpoint) + .map_err(map_err)?; + + let local_addr = ep2sa(&local_endpoint); + let peer_addr = ep2sa(&remote_endpoint); + let tcp = TcpStream { + handle, + reactor, + local_addr, + peer_addr, + }; + + tcp.reactor.notify(); + future::poll_fn(|cx| tcp.poll_connected(cx)).await?; + + Ok(tcp) + } + + fn accept(listener: &mut TcpListener) -> io::Result<(TcpStream, SocketAddr)> { + let reactor = listener.reactor.clone(); + let new_handle = reactor.socket_allocator().new_tcp_socket(); + { + let mut new_socket = reactor.get_socket::(*new_handle); + new_socket.listen(listener.local_addr).map_err(map_err)?; + } + let (peer_addr, local_addr) = { + let socket = reactor.get_socket::(*listener.handle); + ( + // should be Some, because the state is Established + ep2sa(&socket.remote_endpoint().unwrap()), + ep2sa(&socket.local_endpoint().unwrap()), + ) + }; + + Ok(( + TcpStream { + handle: replace(&mut listener.handle, new_handle), + reactor: reactor.clone(), + local_addr, + peer_addr, + }, + peer_addr, + )) + } + + pub fn local_addr(&self) -> io::Result { + Ok(self.local_addr) + } + pub fn peer_addr(&self) -> io::Result { + Ok(self.peer_addr) + } + pub fn poll_connected(&self, cx: &mut Context<'_>) -> Poll> { + let mut socket = self.reactor.get_socket::(*self.handle); + if socket.state() == tcp::State::Established { + return Poll::Ready(Ok(())); + } + socket.register_send_waker(cx.waker()); + Poll::Pending + } +} + +impl AsyncRead for TcpStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let mut socket = self.reactor.get_socket::(*self.handle); + if !socket.may_recv() { + return Poll::Ready(Ok(())); + } + if socket.can_recv() { + let read = socket + .recv_slice(buf.initialize_unfilled()) + .map_err(map_err)?; + self.reactor.notify(); + buf.advance(read); + return Poll::Ready(Ok(())); + } + socket.register_recv_waker(cx.waker()); + Poll::Pending + } +} + +impl AsyncWrite for TcpStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let mut socket = self.reactor.get_socket::(*self.handle); + if !socket.may_send() { + return Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())); + } + if socket.can_send() { + let r = socket.send_slice(buf).map_err(map_err)?; + self.reactor.notify(); + return Poll::Ready(Ok(r)); + } + socket.register_send_waker(cx.waker()); + Poll::Pending + } + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut socket = self.reactor.get_socket::(*self.handle); + if !socket.may_send() { + return Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())); + } + if socket.send_queue() == 0 { + return Poll::Ready(Ok(())); + } + socket.register_send_waker(cx.waker()); + Poll::Pending + } + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut socket = self.reactor.get_socket::(*self.handle); + + if socket.is_open() { + socket.close(); + self.reactor.notify(); + } + if socket.state() == tcp::State::Closed { + return Poll::Ready(Ok(())); + } + + socket.register_send_waker(cx.waker()); + Poll::Pending + } +} diff --git a/rust/easytier/src/gateway/tokio_smoltcp/socket_allocator.rs b/rust/easytier/src/gateway/tokio_smoltcp/socket_allocator.rs new file mode 100644 index 0000000..a95602f --- /dev/null +++ b/rust/easytier/src/gateway/tokio_smoltcp/socket_allocator.rs @@ -0,0 +1,91 @@ +use parking_lot::Mutex; +use smoltcp::{ + iface::{SocketHandle as InnerSocketHandle, SocketSet}, + socket::tcp, + time::Duration, +}; +use std::{ + ops::{Deref, DerefMut}, + sync::Arc, +}; + +/// `BufferSize` is used to configure the size of the socket buffer. +#[derive(Debug, Clone, Copy)] +pub struct BufferSize { + pub tcp_rx_size: usize, + pub tcp_tx_size: usize, +} + +impl Default for BufferSize { + fn default() -> Self { + BufferSize { + tcp_rx_size: 8192, + tcp_tx_size: 8192, + } + } +} + +type SharedSocketSet = Arc>>; + +#[derive(Clone)] +pub struct SocketAlloctor { + sockets: SharedSocketSet, + buffer_size: BufferSize, +} + +impl SocketAlloctor { + pub(crate) fn new(buffer_size: BufferSize) -> SocketAlloctor { + let sockets = Arc::new(Mutex::new(SocketSet::new(Vec::new()))); + SocketAlloctor { + sockets, + buffer_size, + } + } + pub(crate) fn sockets(&self) -> &SharedSocketSet { + &self.sockets + } + pub fn new_tcp_socket(&self) -> SocketHandle { + let mut set = self.sockets.lock(); + let handle = set.add(self.alloc_tcp_socket()); + SocketHandle::new(handle, self.sockets.clone()) + } + fn alloc_tcp_socket(&self) -> tcp::Socket<'static> { + let rx_buffer = tcp::SocketBuffer::new(vec![0; self.buffer_size.tcp_rx_size]); + let tx_buffer = tcp::SocketBuffer::new(vec![0; self.buffer_size.tcp_tx_size]); + let mut tcp = tcp::Socket::new(rx_buffer, tx_buffer); + tcp.set_nagle_enabled(false); + tcp.set_keep_alive(Some(Duration::from_secs(10))); + tcp.set_timeout(Some(Duration::from_secs(60))); + + tcp + } +} + +pub struct SocketHandle(InnerSocketHandle, SharedSocketSet); + +impl SocketHandle { + fn new(inner: InnerSocketHandle, set: SharedSocketSet) -> SocketHandle { + SocketHandle(inner, set) + } +} + +impl Drop for SocketHandle { + fn drop(&mut self) { + let mut iface = self.1.lock(); + iface.remove(self.0); + } +} + +impl Deref for SocketHandle { + type Target = InnerSocketHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SocketHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/rust/easytier/src/gateway/udp_proxy.rs b/rust/easytier/src/gateway/udp_proxy.rs new file mode 100644 index 0000000..441e6b7 --- /dev/null +++ b/rust/easytier/src/gateway/udp_proxy.rs @@ -0,0 +1,447 @@ +use std::{ + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + sync::{atomic::AtomicBool, Arc}, + time::Duration, +}; + +use bytes::{BufMut, BytesMut}; +use cidr::Ipv4Inet; +use crossbeam::atomic::AtomicCell; +use dashmap::DashMap; +use pnet::packet::{ + ip::IpNextHeaderProtocols, + ipv4::Ipv4Packet, + udp::{self, MutableUdpPacket}, + Packet, +}; +use tachyonix::{channel, Receiver, Sender, TrySendError}; +use tokio::{ + net::UdpSocket, + sync::Mutex, + task::{JoinHandle, JoinSet}, + time::timeout, +}; + +use tracing::Level; + +use crate::{ + common::{error::Error, global_ctx::ArcGlobalCtx, scoped_task::ScopedTask, PeerId}, + gateway::ip_reassembler::compose_ipv4_packet, + peers::{peer_manager::PeerManager, PeerPacketFilter}, + tunnel::{ + common::{reserve_buf, setup_sokcet2}, + packet_def::{PacketType, ZCPacket}, + }, +}; + +use super::{ip_reassembler::IpReassembler, CidrSet}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct UdpNatKey { + src_socket: SocketAddr, +} + +#[derive(Debug)] +struct UdpNatEntry { + src_peer_id: PeerId, + my_peer_id: PeerId, + src_socket: SocketAddr, + socket: UdpSocket, + forward_task: Mutex>>, + stopped: AtomicBool, + start_time: std::time::Instant, + last_active_time: AtomicCell, +} + +impl UdpNatEntry { + #[tracing::instrument(err(level = Level::WARN))] + fn new(src_peer_id: PeerId, my_peer_id: PeerId, src_socket: SocketAddr) -> Result { + // TODO: try use src port, so we will be ip restricted nat type + let socket2_socket = socket2::Socket::new( + socket2::Domain::IPV4, + socket2::Type::DGRAM, + Some(socket2::Protocol::UDP), + )?; + let dst_socket_addr = "0.0.0.0:0".parse().unwrap(); + setup_sokcet2(&socket2_socket, &dst_socket_addr)?; + let socket = UdpSocket::from_std(socket2_socket.into())?; + + Ok(Self { + src_peer_id, + my_peer_id, + src_socket, + socket, + forward_task: Mutex::new(None), + stopped: AtomicBool::new(false), + start_time: std::time::Instant::now(), + last_active_time: AtomicCell::new(std::time::Instant::now()), + }) + } + + pub fn stop(&self) { + self.stopped + .store(true, std::sync::atomic::Ordering::Relaxed); + } + + async fn compose_ipv4_packet( + self: &Arc, + packet_sender: &mut Sender, + buf: &mut [u8], + src_v4: &SocketAddrV4, + payload_len: usize, + payload_mtu: usize, + ip_id: u16, + ) -> Result<(), Error> { + let SocketAddr::V4(nat_src_v4) = self.src_socket else { + return Err(Error::Unknown); + }; + + assert_eq!(0, payload_mtu % 8); + + // udp payload is in buf[20 + 8..] + let mut udp_packet = MutableUdpPacket::new(&mut buf[20..28 + payload_len]).unwrap(); + udp_packet.set_source(src_v4.port()); + udp_packet.set_destination(self.src_socket.port()); + udp_packet.set_length(payload_len as u16 + 8); + udp_packet.set_checksum(udp::ipv4_checksum( + &udp_packet.to_immutable(), + src_v4.ip(), + nat_src_v4.ip(), + )); + + compose_ipv4_packet( + &mut buf[..], + src_v4.ip(), + nat_src_v4.ip(), + IpNextHeaderProtocols::Udp, + payload_len + 8, // include udp header + payload_mtu, + ip_id, + |buf| { + let mut p = ZCPacket::new_with_payload(buf); + p.fill_peer_manager_hdr(self.my_peer_id, self.src_peer_id, PacketType::Data as u8); + p.mut_peer_manager_header().unwrap().set_no_proxy(true); + + match packet_sender.try_send(p) { + Err(TrySendError::Closed(e)) => { + tracing::error!("send icmp packet to peer failed: {:?}, may exiting..", e); + Err(Error::Unknown) + } + _ => Ok(()), + } + }, + )?; + + Ok(()) + } + + async fn forward_task( + self: Arc, + mut packet_sender: Sender, + virtual_ipv4: Ipv4Addr, + ) { + let (s, mut r) = tachyonix::channel(128); + + let self_clone = self.clone(); + let recv_task = ScopedTask::from(tokio::spawn(async move { + let mut cur_buf = BytesMut::new(); + loop { + if self_clone + .stopped + .load(std::sync::atomic::Ordering::Relaxed) + { + break; + } + + reserve_buf(&mut cur_buf, 64 * 1024 + 28, 128 * 1024 + 28); + assert_eq!(cur_buf.len(), 0); + unsafe { + cur_buf.advance_mut(28); + } + + let (len, src_socket) = match timeout( + Duration::from_secs(120), + self_clone.socket.recv_buf_from(&mut cur_buf), + ) + .await + { + Ok(Ok(x)) => x, + Ok(Err(err)) => { + tracing::error!(?err, "udp nat recv failed"); + break; + } + Err(err) => { + tracing::error!(?err, "udp nat recv timeout"); + break; + } + }; + + tracing::trace!(?len, ?src_socket, "udp nat packet response received"); + + let ret_buf = cur_buf.split(); + s.send((ret_buf, len, src_socket)).await.unwrap(); + } + })); + + let self_clone = self.clone(); + let send_task = ScopedTask::from(tokio::spawn(async move { + let mut ip_id = 1; + while let Ok((mut packet, len, src_socket)) = r.recv().await { + let SocketAddr::V4(mut src_v4) = src_socket else { + continue; + }; + + self_clone.mark_active(); + + if src_v4.ip().is_loopback() { + src_v4.set_ip(virtual_ipv4); + } + + let Ok(_) = Self::compose_ipv4_packet( + &self_clone, + &mut packet_sender, + &mut packet, + &src_v4, + len, + 1280, + ip_id, + ) + .await + else { + break; + }; + ip_id = ip_id.wrapping_add(1); + } + })); + + let _ = tokio::join!(recv_task, send_task); + + self.stop(); + } + + fn mark_active(&self) { + self.last_active_time.store(std::time::Instant::now()); + } + + fn is_active(&self) -> bool { + self.last_active_time.load().elapsed().as_secs() < 180 + } +} + +#[derive(Debug)] +pub struct UdpProxy { + global_ctx: ArcGlobalCtx, + peer_manager: Arc, + + cidr_set: CidrSet, + + nat_table: Arc>>, + + sender: Sender, + receiver: Mutex>>, + + tasks: Mutex>, + + ip_resemmbler: Arc, +} + +impl UdpProxy { + async fn try_handle_packet(&self, packet: &ZCPacket) -> Option<()> { + if self.cidr_set.is_empty() + && !self.global_ctx.enable_exit_node() + && !self.global_ctx.no_tun() + { + return None; + } + + let _ = self.global_ctx.get_ipv4()?; + let hdr = packet.peer_manager_header().unwrap(); + let is_exit_node = hdr.is_exit_node(); + if hdr.packet_type != PacketType::Data as u8 || hdr.is_no_proxy() { + return None; + }; + + let ipv4 = Ipv4Packet::new(packet.payload())?; + if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Udp { + return None; + } + + if !self.cidr_set.contains_v4(ipv4.get_destination()) + && !is_exit_node + && !(self.global_ctx.no_tun() + && Some(ipv4.get_destination()) + == self.global_ctx.get_ipv4().as_ref().map(Ipv4Inet::address)) + { + return None; + } + + let resembled_buf: Option>; + let udp_packet = if IpReassembler::is_packet_fragmented(&ipv4) { + resembled_buf = + self.ip_resemmbler + .add_fragment(ipv4.get_source(), ipv4.get_destination(), &ipv4); + if resembled_buf.is_none() { + return None; + }; + udp::UdpPacket::new(resembled_buf.as_ref().unwrap())? + } else { + udp::UdpPacket::new(ipv4.payload())? + }; + + tracing::trace!( + ?packet, + ?ipv4, + ?udp_packet, + "udp nat packet request received" + ); + + let nat_key = UdpNatKey { + src_socket: SocketAddr::new(ipv4.get_source().into(), udp_packet.get_source()), + }; + let nat_entry = self + .nat_table + .entry(nat_key) + .or_try_insert_with::(|| { + tracing::info!(?packet, ?ipv4, ?udp_packet, "udp nat table entry created"); + let _g = self.global_ctx.net_ns.guard(); + Ok(Arc::new(UdpNatEntry::new( + hdr.from_peer_id.get(), + hdr.to_peer_id.get(), + nat_key.src_socket, + )?)) + }) + .ok()? + .clone(); + + if nat_entry.forward_task.lock().await.is_none() { + nat_entry + .forward_task + .lock() + .await + .replace(tokio::spawn(UdpNatEntry::forward_task( + nat_entry.clone(), + self.sender.clone(), + self.global_ctx.get_ipv4().map(|x| x.address())?, + ))); + } + + nat_entry.mark_active(); + + // TODO: should it be async. + let dst_socket = if Some(ipv4.get_destination()) + == self.global_ctx.get_ipv4().as_ref().map(Ipv4Inet::address) + { + format!("127.0.0.1:{}", udp_packet.get_destination()) + .parse() + .unwrap() + } else { + SocketAddr::new(ipv4.get_destination().into(), udp_packet.get_destination()) + }; + + let send_ret = { + let _g = self.global_ctx.net_ns.guard(); + nat_entry + .socket + .send_to(udp_packet.payload(), dst_socket) + .await + }; + + if let Err(send_err) = send_ret { + tracing::error!( + ?send_err, + ?nat_key, + ?nat_entry, + ?send_err, + "udp nat send failed" + ); + } + + Some(()) + } +} + +#[async_trait::async_trait] +impl PeerPacketFilter for UdpProxy { + async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option { + if let Some(_) = self.try_handle_packet(&packet).await { + return None; + } else { + return Some(packet); + } + } +} + +impl UdpProxy { + pub fn new( + global_ctx: ArcGlobalCtx, + peer_manager: Arc, + ) -> Result, Error> { + let cidr_set = CidrSet::new(global_ctx.clone()); + let (sender, receiver) = channel(1024); + let ret = Self { + global_ctx, + peer_manager, + cidr_set, + nat_table: Arc::new(DashMap::new()), + sender, + receiver: Mutex::new(Some(receiver)), + tasks: Mutex::new(JoinSet::new()), + ip_resemmbler: Arc::new(IpReassembler::new(Duration::from_secs(10))), + }; + Ok(Arc::new(ret)) + } + + pub async fn start(self: &Arc) -> Result<(), Error> { + self.peer_manager + .add_packet_process_pipeline(Box::new(self.clone())) + .await; + + // clean up nat table + let nat_table = self.nat_table.clone(); + self.tasks.lock().await.spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(15)).await; + nat_table.retain(|_, v| { + if !v.is_active() { + tracing::info!(?v, "udp nat table entry removed"); + v.stop(); + false + } else { + true + } + }); + } + }); + + let ip_resembler = self.ip_resemmbler.clone(); + self.tasks.lock().await.spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(1)).await; + ip_resembler.remove_expired_packets(); + } + }); + + // forward packets to peer manager + let mut receiver = self.receiver.lock().await.take().unwrap(); + let peer_manager = self.peer_manager.clone(); + self.tasks.lock().await.spawn(async move { + while let Ok(msg) = receiver.recv().await { + let to_peer_id: PeerId = msg.peer_manager_header().unwrap().to_peer_id.get(); + tracing::trace!(?msg, ?to_peer_id, "udp nat packet response send"); + let ret = peer_manager.send_msg(msg, to_peer_id).await; + if ret.is_err() { + tracing::error!("send icmp packet to peer failed: {:?}", ret); + } + } + }); + Ok(()) + } +} + +impl Drop for UdpProxy { + fn drop(&mut self) { + for v in self.nat_table.iter() { + v.stop(); + } + } +} diff --git a/rust/easytier/src/instance/instance.rs b/rust/easytier/src/instance/instance.rs new file mode 100644 index 0000000..9460f14 --- /dev/null +++ b/rust/easytier/src/instance/instance.rs @@ -0,0 +1,611 @@ +use std::any::Any; +use std::collections::HashSet; +use std::net::Ipv4Addr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Weak}; + +use anyhow::Context; +use cidr::Ipv4Inet; + +use tokio::{sync::Mutex, task::JoinSet}; + +use crate::common::config::ConfigLoader; +use crate::common::error::Error; +use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtx, GlobalCtxEvent}; +use crate::common::PeerId; +use crate::connector::direct::DirectConnectorManager; +use crate::connector::manual::{ConnectorManagerRpcService, ManualConnectorManager}; +use crate::connector::udp_hole_punch::UdpHolePunchConnector; +use crate::gateway::icmp_proxy::IcmpProxy; +use crate::gateway::kcp_proxy::{KcpProxyDst, KcpProxyDstRpcService, KcpProxySrc}; +use crate::gateway::tcp_proxy::{NatDstTcpConnector, TcpProxy, TcpProxyRpcService}; +use crate::gateway::udp_proxy::UdpProxy; +use crate::peer_center::instance::PeerCenterInstance; +use crate::peers::peer_conn::PeerConnId; +use crate::peers::peer_manager::{PeerManager, RouteAlgoType}; +use crate::peers::rpc_service::PeerManagerRpcService; +use crate::peers::{create_packet_recv_chan, recv_packet_from_chan, PacketRecvChanReceiver}; +use crate::proto::cli::VpnPortalRpc; +use crate::proto::cli::{GetVpnPortalInfoRequest, GetVpnPortalInfoResponse, VpnPortalInfo}; +use crate::proto::peer_rpc::PeerCenterRpcServer; +use crate::proto::rpc_impl::standalone::StandAloneServer; +use crate::proto::rpc_types; +use crate::proto::rpc_types::controller::BaseController; +use crate::tunnel::tcp::TcpTunnelListener; +use crate::vpn_portal::{self, VpnPortal}; + +use super::listeners::ListenerManager; + +#[cfg(feature = "socks5")] +use crate::gateway::socks5::Socks5Server; + +#[derive(Clone)] +struct IpProxy { + tcp_proxy: Arc>, + icmp_proxy: Arc, + udp_proxy: Arc, + global_ctx: ArcGlobalCtx, + started: Arc, +} + +impl IpProxy { + fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc) -> Result { + let tcp_proxy = TcpProxy::new(peer_manager.clone(), NatDstTcpConnector {}); + let icmp_proxy = IcmpProxy::new(global_ctx.clone(), peer_manager.clone()) + .with_context(|| "create icmp proxy failed")?; + let udp_proxy = UdpProxy::new(global_ctx.clone(), peer_manager.clone()) + .with_context(|| "create udp proxy failed")?; + Ok(IpProxy { + tcp_proxy, + icmp_proxy, + udp_proxy, + global_ctx, + started: Arc::new(AtomicBool::new(false)), + }) + } + + async fn start(&self) -> Result<(), Error> { + if (self.global_ctx.get_proxy_cidrs().is_empty() + || self.global_ctx.proxy_forward_by_system() + || self.started.load(Ordering::Relaxed)) + && !self.global_ctx.enable_exit_node() + && !self.global_ctx.no_tun() + { + return Ok(()); + } + + self.started.store(true, Ordering::Relaxed); + self.tcp_proxy.start(true).await?; + self.icmp_proxy.start().await?; + self.udp_proxy.start().await?; + Ok(()) + } +} + +#[cfg(feature = "tun")] +type NicCtx = super::virtual_nic::NicCtx; +#[cfg(not(feature = "tun"))] +struct NicCtx; +#[cfg(not(feature = "tun"))] +impl NicCtx { + pub fn new( + _global_ctx: ArcGlobalCtx, + _peer_manager: &Arc, + _peer_packet_receiver: Arc>, + ) -> Self { + Self + } + + pub async fn run(&mut self, _ipv4_addr: Ipv4Addr) -> Result<(), Error> { + Ok(()) + } +} + +type ArcNicCtx = Arc>>>; + +pub struct Instance { + inst_name: String, + + id: uuid::Uuid, + + nic_ctx: ArcNicCtx, + + peer_packet_receiver: Arc>, + peer_manager: Arc, + listener_manager: Arc>>, + conn_manager: Arc, + direct_conn_manager: Arc, + udp_hole_puncher: Arc>, + + ip_proxy: Option, + + kcp_proxy_src: Option, + kcp_proxy_dst: Option, + + peer_center: Arc, + + vpn_portal: Arc>>, + + #[cfg(feature = "socks5")] + socks5_server: Arc, + + rpc_server: Option>, + + global_ctx: ArcGlobalCtx, +} + +impl Instance { + pub fn new(config: impl ConfigLoader + Send + Sync + 'static) -> Self { + let global_ctx = Arc::new(GlobalCtx::new(config)); + + tracing::info!( + "[INIT] instance creating. config: {}", + global_ctx.config.dump() + ); + + let (peer_packet_sender, peer_packet_receiver) = create_packet_recv_chan(); + + let id = global_ctx.get_id(); + + let peer_manager = Arc::new(PeerManager::new( + RouteAlgoType::Ospf, + global_ctx.clone(), + peer_packet_sender.clone(), + )); + + let listener_manager = Arc::new(Mutex::new(ListenerManager::new( + global_ctx.clone(), + peer_manager.clone(), + ))); + + let conn_manager = Arc::new(ManualConnectorManager::new( + global_ctx.clone(), + peer_manager.clone(), + )); + + let mut direct_conn_manager = + DirectConnectorManager::new(global_ctx.clone(), peer_manager.clone()); + direct_conn_manager.run(); + + let udp_hole_puncher = UdpHolePunchConnector::new(peer_manager.clone()); + + let peer_center = Arc::new(PeerCenterInstance::new(peer_manager.clone())); + + #[cfg(feature = "wireguard")] + let vpn_portal_inst = vpn_portal::wireguard::WireGuard::default(); + #[cfg(not(feature = "wireguard"))] + let vpn_portal_inst = vpn_portal::NullVpnPortal; + + #[cfg(feature = "socks5")] + let socks5_server = Socks5Server::new(global_ctx.clone(), peer_manager.clone(), None); + + let rpc_server = global_ctx.config.get_rpc_portal().and_then(|s| { + Some(StandAloneServer::new(TcpTunnelListener::new( + format!("tcp://{}", s).parse().unwrap(), + ))) + }); + + Instance { + inst_name: global_ctx.inst_name.clone(), + id, + + peer_packet_receiver: Arc::new(Mutex::new(peer_packet_receiver)), + nic_ctx: Arc::new(Mutex::new(None)), + + peer_manager, + listener_manager, + conn_manager, + direct_conn_manager: Arc::new(direct_conn_manager), + udp_hole_puncher: Arc::new(Mutex::new(udp_hole_puncher)), + + ip_proxy: None, + kcp_proxy_src: None, + kcp_proxy_dst: None, + + peer_center, + + vpn_portal: Arc::new(Mutex::new(Box::new(vpn_portal_inst))), + + #[cfg(feature = "socks5")] + socks5_server, + + rpc_server, + + global_ctx, + } + } + + pub fn get_conn_manager(&self) -> Arc { + self.conn_manager.clone() + } + + async fn add_initial_peers(&mut self) -> Result<(), Error> { + for peer in self.global_ctx.config.get_peers().iter() { + self.get_conn_manager() + .add_connector_by_url(peer.uri.as_str()) + .await?; + } + Ok(()) + } + + // use a mock nic ctx to consume packets. + async fn clear_nic_ctx( + arc_nic_ctx: ArcNicCtx, + packet_recv: Arc>, + ) { + let _ = arc_nic_ctx.lock().await.take(); + + let mut tasks = JoinSet::new(); + tasks.spawn(async move { + let mut packet_recv = packet_recv.lock().await; + while let Ok(packet) = recv_packet_from_chan(&mut packet_recv).await { + tracing::trace!("packet consumed by mock nic ctx: {:?}", packet); + } + }); + arc_nic_ctx.lock().await.replace(Box::new(tasks)); + + tracing::debug!("nic ctx cleared."); + } + + async fn use_new_nic_ctx(arc_nic_ctx: ArcNicCtx, nic_ctx: NicCtx) { + let mut g = arc_nic_ctx.lock().await; + *g = Some(Box::new(nic_ctx)); + tracing::debug!("nic ctx updated."); + } + + // Warning, if there is an IP conflict in the network when using DHCP, the IP will be automatically changed. + fn check_dhcp_ip_conflict(&self) { + use rand::Rng; + let peer_manager_c = self.peer_manager.clone(); + let global_ctx_c = self.get_global_ctx(); + let nic_ctx = self.nic_ctx.clone(); + let _peer_packet_receiver = self.peer_packet_receiver.clone(); + tokio::spawn(async move { + let default_ipv4_addr = Ipv4Inet::new(Ipv4Addr::new(10, 126, 126, 0), 24).unwrap(); + let mut current_dhcp_ip: Option = None; + let mut next_sleep_time = 0; + loop { + tokio::time::sleep(std::time::Duration::from_secs(next_sleep_time)).await; + + // do not allocate ip if no peer connected + let routes = peer_manager_c.list_routes().await; + if routes.is_empty() { + next_sleep_time = 1; + continue; + } else { + next_sleep_time = rand::thread_rng().gen_range(5..10); + } + + let mut used_ipv4 = HashSet::new(); + for route in routes { + let Some(peer_ipv4_addr) = route.ipv4_addr else { + continue; + }; + + used_ipv4.insert(peer_ipv4_addr.into()); + } + + let dhcp_inet = used_ipv4.iter().next().unwrap_or(&default_ipv4_addr); + // if old ip is already in this subnet and not conflicted, use it + if let Some(ip) = current_dhcp_ip { + if ip.network() == dhcp_inet.network() && !used_ipv4.contains(&ip) { + continue; + } + } + + // find an available ip in the subnet + let candidate_ipv4_addr = dhcp_inet.network().iter().find(|ip| { + ip.address() != dhcp_inet.first_address() + && ip.address() != dhcp_inet.last_address() + && !used_ipv4.contains(ip) + }); + + if current_dhcp_ip == candidate_ipv4_addr { + continue; + } + + let last_ip = current_dhcp_ip.clone(); + tracing::debug!( + ?current_dhcp_ip, + ?candidate_ipv4_addr, + "dhcp start changing ip" + ); + + Self::clear_nic_ctx(nic_ctx.clone(), _peer_packet_receiver.clone()).await; + + if let Some(ip) = candidate_ipv4_addr { + if global_ctx_c.no_tun() { + current_dhcp_ip = Some(ip); + global_ctx_c.set_ipv4(Some(ip)); + global_ctx_c + .issue_event(GlobalCtxEvent::DhcpIpv4Changed(last_ip, Some(ip))); + continue; + } + + #[cfg(not(target_os = "android"))] + { + let mut new_nic_ctx = NicCtx::new( + global_ctx_c.clone(), + &peer_manager_c, + _peer_packet_receiver.clone(), + ); + if let Err(e) = new_nic_ctx.run(ip).await { + tracing::error!( + ?current_dhcp_ip, + ?candidate_ipv4_addr, + ?e, + "add ip failed" + ); + global_ctx_c.set_ipv4(None); + continue; + } + Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx).await; + } + + current_dhcp_ip = Some(ip); + global_ctx_c.set_ipv4(Some(ip)); + global_ctx_c.issue_event(GlobalCtxEvent::DhcpIpv4Changed(last_ip, Some(ip))); + } else { + current_dhcp_ip = None; + global_ctx_c.set_ipv4(None); + global_ctx_c.issue_event(GlobalCtxEvent::DhcpIpv4Conflicted(last_ip)); + } + } + }); + } + + pub async fn run(&mut self) -> Result<(), Error> { + self.listener_manager + .lock() + .await + .prepare_listeners() + .await?; + self.listener_manager.lock().await.run().await?; + self.peer_manager.run().await?; + + Self::clear_nic_ctx(self.nic_ctx.clone(), self.peer_packet_receiver.clone()).await; + + if !self.global_ctx.config.get_flags().no_tun { + #[cfg(not(target_os = "android"))] + if let Some(ipv4_addr) = self.global_ctx.get_ipv4() { + let mut new_nic_ctx = NicCtx::new( + self.global_ctx.clone(), + &self.peer_manager, + self.peer_packet_receiver.clone(), + ); + new_nic_ctx.run(ipv4_addr).await?; + Self::use_new_nic_ctx(self.nic_ctx.clone(), new_nic_ctx).await; + } + } + + if self.global_ctx.config.get_dhcp() { + self.check_dhcp_ip_conflict(); + } + + if self.global_ctx.get_flags().enable_kcp_proxy { + let src_proxy = KcpProxySrc::new(self.get_peer_manager()).await; + src_proxy.start().await; + self.kcp_proxy_src = Some(src_proxy); + } + + if !self.global_ctx.get_flags().disable_kcp_input { + let mut dst_proxy = KcpProxyDst::new(self.get_peer_manager()).await; + dst_proxy.start().await; + self.kcp_proxy_dst = Some(dst_proxy); + } + + // run after tun device created, so listener can bind to tun device, which may be required by win 10 + self.ip_proxy = Some(IpProxy::new( + self.get_global_ctx(), + self.get_peer_manager(), + )?); + self.run_ip_proxy().await?; + + self.udp_hole_puncher.lock().await.run().await?; + + self.peer_center.init().await; + let route_calc = self.peer_center.get_cost_calculator(); + self.peer_manager + .get_route() + .set_route_cost_fn(route_calc) + .await; + + self.add_initial_peers().await?; + + if self.global_ctx.get_vpn_portal_cidr().is_some() { + self.run_vpn_portal().await?; + } + + #[cfg(feature = "socks5")] + self.socks5_server.run().await?; + + self.run_rpc_server().await?; + + Ok(()) + } + + pub async fn run_ip_proxy(&mut self) -> Result<(), Error> { + if self.ip_proxy.is_none() { + return Err(anyhow::anyhow!("ip proxy not enabled.").into()); + } + self.ip_proxy.as_ref().unwrap().start().await?; + Ok(()) + } + + pub async fn run_vpn_portal(&mut self) -> Result<(), Error> { + if self.global_ctx.get_vpn_portal_cidr().is_none() { + return Err(anyhow::anyhow!("vpn portal cidr not set.").into()); + } + self.vpn_portal + .lock() + .await + .start(self.get_global_ctx(), self.get_peer_manager()) + .await?; + Ok(()) + } + + pub fn get_peer_manager(&self) -> Arc { + self.peer_manager.clone() + } + + pub async fn close_peer_conn( + &mut self, + peer_id: PeerId, + conn_id: &PeerConnId, + ) -> Result<(), Error> { + self.peer_manager + .get_peer_map() + .close_peer_conn(peer_id, conn_id) + .await?; + Ok(()) + } + + pub async fn wait(&self) { + self.peer_manager.wait().await; + } + + pub fn id(&self) -> uuid::Uuid { + self.id + } + + pub fn peer_id(&self) -> PeerId { + self.peer_manager.my_peer_id() + } + + fn get_vpn_portal_rpc_service(&self) -> impl VpnPortalRpc + Clone { + #[derive(Clone)] + struct VpnPortalRpcService { + peer_mgr: Weak, + vpn_portal: Weak>>, + } + + #[async_trait::async_trait] + impl VpnPortalRpc for VpnPortalRpcService { + type Controller = BaseController; + + async fn get_vpn_portal_info( + &self, + _: BaseController, + _request: GetVpnPortalInfoRequest, + ) -> Result { + let Some(vpn_portal) = self.vpn_portal.upgrade() else { + return Err(anyhow::anyhow!("vpn portal not available").into()); + }; + + let Some(peer_mgr) = self.peer_mgr.upgrade() else { + return Err(anyhow::anyhow!("peer manager not available").into()); + }; + + let vpn_portal = vpn_portal.lock().await; + let ret = GetVpnPortalInfoResponse { + vpn_portal_info: Some(VpnPortalInfo { + vpn_type: vpn_portal.name(), + client_config: vpn_portal.dump_client_config(peer_mgr).await, + connected_clients: vpn_portal.list_clients().await, + }), + }; + + Ok(ret) + } + } + + VpnPortalRpcService { + peer_mgr: Arc::downgrade(&self.peer_manager), + vpn_portal: Arc::downgrade(&self.vpn_portal), + } + } + + async fn run_rpc_server(&mut self) -> Result<(), Error> { + let Some(_) = self.global_ctx.config.get_rpc_portal() else { + tracing::info!("rpc server not enabled, because rpc_portal is not set."); + return Ok(()); + }; + + use crate::proto::cli::*; + + let peer_mgr = self.peer_manager.clone(); + let conn_manager = self.conn_manager.clone(); + let peer_center = self.peer_center.clone(); + let vpn_portal_rpc = self.get_vpn_portal_rpc_service(); + + let s = self.rpc_server.as_mut().unwrap(); + s.registry().register( + PeerManageRpcServer::new(PeerManagerRpcService::new(peer_mgr)), + "", + ); + s.registry().register( + ConnectorManageRpcServer::new(ConnectorManagerRpcService(conn_manager)), + "", + ); + + s.registry() + .register(PeerCenterRpcServer::new(peer_center.get_rpc_service()), ""); + s.registry() + .register(VpnPortalRpcServer::new(vpn_portal_rpc), ""); + + if let Some(ip_proxy) = self.ip_proxy.as_ref() { + s.registry().register( + TcpProxyRpcServer::new(TcpProxyRpcService::new(ip_proxy.tcp_proxy.clone())), + "tcp", + ); + } + if let Some(kcp_proxy) = self.kcp_proxy_src.as_ref() { + s.registry().register( + TcpProxyRpcServer::new(TcpProxyRpcService::new(kcp_proxy.get_tcp_proxy())), + "kcp_src", + ); + } + + if let Some(kcp_proxy) = self.kcp_proxy_dst.as_ref() { + s.registry().register( + TcpProxyRpcServer::new(KcpProxyDstRpcService::new(kcp_proxy)), + "kcp_dst", + ); + } + + let _g = self.global_ctx.net_ns.guard(); + Ok(s.serve().await.with_context(|| "rpc server start failed")?) + } + + pub fn get_global_ctx(&self) -> ArcGlobalCtx { + self.global_ctx.clone() + } + + pub fn get_vpn_portal_inst(&self) -> Arc>> { + self.vpn_portal.clone() + } + + pub fn get_nic_ctx(&self) -> ArcNicCtx { + self.nic_ctx.clone() + } + + pub fn get_peer_packet_receiver(&self) -> Arc> { + self.peer_packet_receiver.clone() + } + + #[cfg(target_os = "android")] + pub async fn setup_nic_ctx_for_android( + nic_ctx: ArcNicCtx, + global_ctx: ArcGlobalCtx, + peer_manager: Arc, + peer_packet_receiver: Arc>, + fd: i32, + ) -> Result<(), anyhow::Error> { + println!("setup_nic_ctx_for_android, fd: {}", fd); + Self::clear_nic_ctx(nic_ctx.clone(), peer_packet_receiver.clone()).await; + if fd <= 0 { + return Ok(()); + } + let mut new_nic_ctx = NicCtx::new( + global_ctx.clone(), + &peer_manager, + peer_packet_receiver.clone(), + ); + new_nic_ctx + .run_for_android(fd) + .await + .with_context(|| "add ip failed")?; + Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx).await; + Ok(()) + } +} diff --git a/rust/easytier/src/instance/listeners.rs b/rust/easytier/src/instance/listeners.rs new file mode 100644 index 0000000..91aceb2 --- /dev/null +++ b/rust/easytier/src/instance/listeners.rs @@ -0,0 +1,353 @@ +use std::{fmt::Debug, sync::Arc}; + +use async_trait::async_trait; +use tokio::task::JoinSet; + +#[cfg(feature = "quic")] +use crate::tunnel::quic::QUICTunnelListener; +#[cfg(feature = "wireguard")] +use crate::tunnel::wireguard::{WgConfig, WgTunnelListener}; +use crate::{ + common::{ + error::Error, + global_ctx::{ArcGlobalCtx, GlobalCtxEvent}, + netns::NetNS, + }, + peers::peer_manager::PeerManager, + tunnel::{ + ring::RingTunnelListener, tcp::TcpTunnelListener, udp::UdpTunnelListener, Tunnel, + TunnelListener, + }, +}; + +pub fn get_listener_by_url( + l: &url::Url, + _ctx: ArcGlobalCtx, +) -> Result, Error> { + Ok(match l.scheme() { + "tcp" => Box::new(TcpTunnelListener::new(l.clone())), + "udp" => Box::new(UdpTunnelListener::new(l.clone())), + #[cfg(feature = "wireguard")] + "wg" => { + let nid = _ctx.get_network_identity(); + let wg_config = WgConfig::new_from_network_identity( + &nid.network_name, + &nid.network_secret.unwrap_or_default(), + ); + Box::new(WgTunnelListener::new(l.clone(), wg_config)) + } + #[cfg(feature = "quic")] + "quic" => Box::new(QUICTunnelListener::new(l.clone())), + #[cfg(feature = "websocket")] + "ws" | "wss" => { + use crate::tunnel::websocket::WSTunnelListener; + Box::new(WSTunnelListener::new(l.clone())) + } + _ => { + return Err(Error::InvalidUrl(l.to_string())); + } + }) +} + +#[async_trait] +pub trait TunnelHandlerForListener { + async fn handle_tunnel(&self, tunnel: Box) -> Result<(), Error>; +} + +#[async_trait] +impl TunnelHandlerForListener for PeerManager { + #[tracing::instrument] + async fn handle_tunnel(&self, tunnel: Box) -> Result<(), Error> { + self.add_tunnel_as_server(tunnel).await + } +} + +pub trait ListenerCreatorTrait: Fn() -> Box + Send + Sync {} +impl ListenerCreatorTrait for T where T: Fn() -> Box + Send {} +pub type ListenerCreator = Box; + +#[derive(Clone)] +struct ListenerFactory { + creator_fn: Arc, + must_succ: bool, +} + +pub struct ListenerManager { + global_ctx: ArcGlobalCtx, + net_ns: NetNS, + listeners: Vec, + peer_manager: Arc, + + tasks: JoinSet<()>, +} + +impl ListenerManager { + pub fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc) -> Self { + Self { + global_ctx: global_ctx.clone(), + net_ns: global_ctx.net_ns.clone(), + listeners: Vec::new(), + peer_manager, + tasks: JoinSet::new(), + } + } + + pub async fn prepare_listeners(&mut self) -> Result<(), Error> { + let self_id = self.global_ctx.get_id(); + self.add_listener( + move || { + Box::new(RingTunnelListener::new( + format!("ring://{}", self_id).parse().unwrap(), + )) + }, + true, + ) + .await?; + + for l in self.global_ctx.config.get_listener_uris().iter() { + let l = l.clone(); + let Ok(_) = get_listener_by_url(&l, self.global_ctx.clone()) else { + let msg = format!("failed to get listener by url: {}, maybe not supported", l); + self.global_ctx + .issue_event(GlobalCtxEvent::ListenerAddFailed(l.clone(), msg)); + continue; + }; + let ctx = self.global_ctx.clone(); + self.add_listener(move || get_listener_by_url(&l, ctx.clone()).unwrap(), true) + .await?; + } + + if self.global_ctx.config.get_flags().enable_ipv6 { + let ipv6_listener = self.global_ctx.config.get_flags().ipv6_listener.clone(); + let _ = self + .add_listener( + move || { + Box::new(UdpTunnelListener::new( + ipv6_listener.clone().parse().unwrap(), + )) + }, + false, + ) + .await?; + } + + Ok(()) + } + + pub async fn add_listener( + &mut self, + creator: C, + must_succ: bool, + ) -> Result<(), Error> { + self.listeners.push(ListenerFactory { + creator_fn: Arc::new(Box::new(creator)), + must_succ, + }); + Ok(()) + } + + #[tracing::instrument(skip(creator))] + async fn run_listener( + creator: Arc, + peer_manager: Arc, + global_ctx: ArcGlobalCtx, + ) { + loop { + let mut l = (creator)(); + let _g = global_ctx.net_ns.guard(); + match l.listen().await { + Ok(_) => { + global_ctx.add_running_listener(l.local_url()); + global_ctx.issue_event(GlobalCtxEvent::ListenerAdded(l.local_url())); + } + Err(e) => { + global_ctx.issue_event(GlobalCtxEvent::ListenerAddFailed( + l.local_url(), + format!("error: {:?}, retry listen later...", e), + )); + tracing::error!(?e, ?l, "listener listen error"); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + continue; + } + } + loop { + let ret = match l.accept().await { + Ok(ret) => ret, + Err(e) => { + global_ctx.issue_event(GlobalCtxEvent::ListenerAcceptFailed( + l.local_url(), + format!("error: {:?}, retry listen later...", e), + )); + tracing::error!(?e, ?l, "listener accept error"); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + break; + } + }; + + let tunnel_info = ret.info().unwrap(); + global_ctx.issue_event(GlobalCtxEvent::ConnectionAccepted( + tunnel_info + .local_addr + .clone() + .unwrap_or_default() + .to_string(), + tunnel_info + .remote_addr + .clone() + .unwrap_or_default() + .to_string(), + )); + tracing::info!(ret = ?ret, "conn accepted"); + let peer_manager = peer_manager.clone(); + let global_ctx = global_ctx.clone(); + tokio::spawn(async move { + let server_ret = peer_manager.handle_tunnel(ret).await; + if let Err(e) = &server_ret { + global_ctx.issue_event(GlobalCtxEvent::ConnectionError( + tunnel_info.local_addr.unwrap_or_default().to_string(), + tunnel_info.remote_addr.unwrap_or_default().to_string(), + e.to_string(), + )); + tracing::error!(error = ?e, "handle conn error"); + } + }); + } + } + } + + pub async fn run(&mut self) -> Result<(), Error> { + for listener in &self.listeners { + self.tasks.spawn(Self::run_listener( + listener.creator_fn.clone(), + self.peer_manager.clone(), + self.global_ctx.clone(), + )); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicI32, Ordering}; + + use futures::{SinkExt, StreamExt}; + use tokio::time::timeout; + + use crate::{ + common::global_ctx::tests::get_mock_global_ctx, + tunnel::{packet_def::ZCPacket, ring::RingTunnelConnector, TunnelConnector, TunnelError}, + }; + + use super::*; + + #[derive(Debug)] + struct MockListenerHandler {} + + #[async_trait] + impl TunnelHandlerForListener for MockListenerHandler { + async fn handle_tunnel(&self, tunnel: Box) -> Result<(), Error> { + let data = "abc"; + let (_recv, mut send) = tunnel.split(); + + let zc_packet = ZCPacket::new_with_payload(data.as_bytes()); + send.send(zc_packet).await.unwrap(); + Err(Error::Unknown) + } + } + + #[tokio::test] + async fn handle_error_in_accept() { + let handler = Arc::new(MockListenerHandler {}); + let mut listener_mgr = ListenerManager::new(get_mock_global_ctx(), handler.clone()); + + let ring_id = format!("ring://{}", uuid::Uuid::new_v4()); + + let ring_id_clone = ring_id.clone(); + listener_mgr + .add_listener( + move || Box::new(RingTunnelListener::new(ring_id_clone.parse().unwrap())), + true, + ) + .await + .unwrap(); + listener_mgr.run().await.unwrap(); + + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let connect_once = |ring_id| async move { + let tunnel = RingTunnelConnector::new(ring_id).connect().await.unwrap(); + let (mut recv, _send) = tunnel.split(); + assert_eq!( + recv.next().await.unwrap().unwrap().payload(), + "abc".as_bytes() + ); + tunnel + }; + + timeout(std::time::Duration::from_secs(1), async move { + connect_once(ring_id.parse().unwrap()).await; + // handle tunnel fail should not impact the second connect + connect_once(ring_id.parse().unwrap()).await; + }) + .await + .unwrap(); + } + + #[tokio::test] + async fn retry_listen() { + let counter = Arc::new(AtomicI32::new(0)); + let drop_counter = Arc::new(AtomicI32::new(0)); + struct MockListener { + counter: Arc, + drop_counter: Arc, + } + + #[async_trait::async_trait] + impl TunnelListener for MockListener { + fn local_url(&self) -> url::Url { + "mock://".parse().unwrap() + } + + async fn listen(&mut self) -> Result<(), TunnelError> { + self.counter.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + + async fn accept(&mut self) -> Result, TunnelError> { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + Err(TunnelError::BufferFull) + } + } + + impl Drop for MockListener { + fn drop(&mut self) { + self.drop_counter.fetch_add(1, Ordering::Relaxed); + } + } + + let handler = Arc::new(MockListenerHandler {}); + let mut listener_mgr = ListenerManager::new(get_mock_global_ctx(), handler.clone()); + let counter_clone = counter.clone(); + let drop_counter_clone = drop_counter.clone(); + listener_mgr + .add_listener( + move || { + Box::new(MockListener { + counter: counter_clone.clone(), + drop_counter: drop_counter_clone.clone(), + }) + }, + true, + ) + .await + .unwrap(); + listener_mgr.run().await.unwrap(); + + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + + assert!(counter.load(Ordering::Relaxed) >= 2); + assert!(drop_counter.load(Ordering::Relaxed) >= 1); + } +} diff --git a/rust/easytier/src/instance/mod.rs b/rust/easytier/src/instance/mod.rs new file mode 100644 index 0000000..17f7f32 --- /dev/null +++ b/rust/easytier/src/instance/mod.rs @@ -0,0 +1,5 @@ +pub mod instance; +pub mod listeners; + +#[cfg(feature = "tun")] +pub mod virtual_nic; diff --git a/rust/easytier/src/instance/virtual_nic.rs b/rust/easytier/src/instance/virtual_nic.rs new file mode 100644 index 0000000..eda8acf --- /dev/null +++ b/rust/easytier/src/instance/virtual_nic.rs @@ -0,0 +1,819 @@ +use std::{ + io, + net::Ipv4Addr, + pin::Pin, + sync::{Arc, Weak}, + task::{Context, Poll}, +}; + +use crate::{ + common::{ + error::Error, + global_ctx::{ArcGlobalCtx, GlobalCtxEvent}, + ifcfg::{IfConfiger, IfConfiguerTrait}, + }, + peers::{peer_manager::PeerManager, recv_packet_from_chan, PacketRecvChanReceiver}, + tunnel::{ + common::{reserve_buf, FramedWriter, TunnelWrapper, ZCPacketToBytes}, + packet_def::{ZCPacket, ZCPacketType, TAIL_RESERVED_SIZE}, + StreamItem, Tunnel, TunnelError, ZCPacketSink, ZCPacketStream, + }, +}; + +use byteorder::WriteBytesExt as _; +use bytes::{BufMut, BytesMut}; +use futures::{lock::BiLock, ready, SinkExt, Stream, StreamExt}; +use pin_project_lite::pin_project; +use pnet::packet::ipv4::Ipv4Packet; +use tokio::{ + io::{AsyncRead, AsyncWrite, ReadBuf}, + sync::Mutex, + task::JoinSet, +}; +use tokio_util::bytes::Bytes; +use tun::{AbstractDevice, AsyncDevice, Configuration, Layer}; +use zerocopy::{NativeEndian, NetworkEndian}; + +pin_project! { + pub struct TunStream { + #[pin] + l: BiLock, + cur_buf: BytesMut, + has_packet_info: bool, + payload_offset: usize, + } +} + +impl TunStream { + pub fn new(l: BiLock, has_packet_info: bool) -> Self { + let mut payload_offset = ZCPacketType::NIC.get_packet_offsets().payload_offset; + if has_packet_info { + payload_offset -= 4; + } + Self { + l, + cur_buf: BytesMut::new(), + has_packet_info, + payload_offset, + } + } +} + +impl Stream for TunStream { + type Item = StreamItem; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut self_mut = self.project(); + let mut g = ready!(self_mut.l.poll_lock(cx)); + reserve_buf(&mut self_mut.cur_buf, 2500, 32 * 1024); + if self_mut.cur_buf.len() == 0 { + unsafe { + self_mut.cur_buf.set_len(*self_mut.payload_offset); + } + } + let buf = self_mut.cur_buf.chunk_mut().as_mut_ptr(); + let buf = unsafe { std::slice::from_raw_parts_mut(buf, 2500) }; + let mut buf = ReadBuf::new(buf); + + let ret = ready!(g.as_pin_mut().poll_read(cx, &mut buf)); + let len = buf.filled().len(); + if len == 0 { + return Poll::Ready(None); + } + unsafe { self_mut.cur_buf.advance_mut(len + TAIL_RESERVED_SIZE) }; + + let mut ret_buf = self_mut.cur_buf.split(); + let cur_len = ret_buf.len(); + ret_buf.truncate(cur_len - TAIL_RESERVED_SIZE); + + match ret { + Ok(_) => Poll::Ready(Some(Ok(ZCPacket::new_from_buf(ret_buf, ZCPacketType::NIC)))), + Err(err) => { + println!("tun stream error: {:?}", err); + Poll::Ready(None) + } + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +enum PacketProtocol { + #[default] + IPv4, + IPv6, + Other(u8), +} + +// Note: the protocol in the packet information header is platform dependent. +impl PacketProtocol { + #[cfg(any(target_os = "linux", target_os = "android"))] + fn into_pi_field(self) -> Result { + use nix::libc; + match self { + PacketProtocol::IPv4 => Ok(libc::ETH_P_IP as u16), + PacketProtocol::IPv6 => Ok(libc::ETH_P_IPV6 as u16), + PacketProtocol::Other(_) => Err(io::Error::new( + io::ErrorKind::Other, + "neither an IPv4 nor IPv6 packet", + )), + } + } + + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))] + fn into_pi_field(self) -> Result { + use nix::libc; + match self { + PacketProtocol::IPv4 => Ok(libc::PF_INET as u16), + PacketProtocol::IPv6 => Ok(libc::PF_INET6 as u16), + PacketProtocol::Other(_) => Err(io::Error::new( + io::ErrorKind::Other, + "neither an IPv4 nor IPv6 packet", + )), + } + } + + #[cfg(target_os = "windows")] + fn into_pi_field(self) -> Result { + unimplemented!() + } +} + +/// Infer the protocol based on the first nibble in the packet buffer. +fn infer_proto(buf: &[u8]) -> PacketProtocol { + match buf[0] >> 4 { + 4 => PacketProtocol::IPv4, + 6 => PacketProtocol::IPv6, + p => PacketProtocol::Other(p), + } +} + +struct TunZCPacketToBytes { + has_packet_info: bool, +} + +impl TunZCPacketToBytes { + pub fn new(has_packet_info: bool) -> Self { + Self { has_packet_info } + } + + pub fn fill_packet_info( + &self, + mut buf: &mut [u8], + proto: PacketProtocol, + ) -> Result<(), io::Error> { + // flags is always 0 + buf.write_u16::(0)?; + // write the protocol as network byte order + buf.write_u16::(proto.into_pi_field()?)?; + Ok(()) + } +} + +impl ZCPacketToBytes for TunZCPacketToBytes { + fn into_bytes(&self, zc_packet: ZCPacket) -> Result { + let payload_offset = zc_packet.payload_offset(); + let mut inner = zc_packet.inner(); + // we have peer manager header, so payload offset must larger than 4 + assert!(payload_offset >= 4); + + let ret = if self.has_packet_info { + let mut inner = inner.split_off(payload_offset - 4); + let proto = infer_proto(&inner[4..]); + self.fill_packet_info(&mut inner[0..4], proto)?; + inner + } else { + inner.split_off(payload_offset) + }; + + tracing::debug!(?ret, ?payload_offset, "convert zc packet to tun packet"); + + Ok(ret.into()) + } +} + +pin_project! { + pub struct TunAsyncWrite { + #[pin] + l: BiLock, + } +} + +impl AsyncWrite for TunAsyncWrite { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let self_mut = self.project(); + let mut g = ready!(self_mut.l.poll_lock(cx)); + g.as_pin_mut().poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let self_mut = self.project(); + let mut g = ready!(self_mut.l.poll_lock(cx)); + g.as_pin_mut().poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let self_mut = self.project(); + let mut g = ready!(self_mut.l.poll_lock(cx)); + g.as_pin_mut().poll_shutdown(cx) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + let self_mut = self.project(); + let mut g = ready!(self_mut.l.poll_lock(cx)); + g.as_pin_mut().poll_write_vectored(cx, bufs) + } + + fn is_write_vectored(&self) -> bool { + true + } +} + +pub struct VirtualNic { + global_ctx: ArcGlobalCtx, + + ifname: Option, + ifcfg: Box, +} + +#[cfg(target_os = "windows")] +pub fn checkreg(dev_name: &str) -> io::Result<()> { + use winreg::{enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS, RegKey}; + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let profiles_key = hklm.open_subkey_with_flags( + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles", + KEY_ALL_ACCESS, + )?; + let unmanaged_key = hklm.open_subkey_with_flags( + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Signatures\\Unmanaged", + KEY_ALL_ACCESS, + )?; + // collect subkeys to delete + let mut keys_to_delete = Vec::new(); + let mut keys_to_delete_unmanaged = Vec::new(); + for subkey_name in profiles_key.enum_keys().filter_map(Result::ok) { + let subkey = profiles_key.open_subkey(&subkey_name)?; + // check if ProfileName contains "et" + match subkey.get_value::("ProfileName") { + Ok(profile_name) => { + if profile_name.contains("et_") + || (!dev_name.is_empty() && dev_name == profile_name) + { + keys_to_delete.push(subkey_name); + } + } + Err(e) => { + tracing::error!( + "Failed to read ProfileName for subkey {}: {}", + subkey_name, + e + ); + } + } + } + for subkey_name in unmanaged_key.enum_keys().filter_map(Result::ok) { + let subkey = unmanaged_key.open_subkey(&subkey_name)?; + // check if ProfileName contains "et" + match subkey.get_value::("Description") { + Ok(profile_name) => { + if profile_name.contains("et_") + || (!dev_name.is_empty() && dev_name == profile_name) + { + keys_to_delete_unmanaged.push(subkey_name); + } + } + Err(e) => { + tracing::error!( + "Failed to read ProfileName for subkey {}: {}", + subkey_name, + e + ); + } + } + } + // delete collected subkeys + if !keys_to_delete.is_empty() { + for subkey_name in keys_to_delete { + match profiles_key.delete_subkey_all(&subkey_name) { + Ok(_) => tracing::trace!("Successfully deleted subkey: {}", subkey_name), + Err(e) => tracing::error!("Failed to delete subkey {}: {}", subkey_name, e), + } + } + } + if !keys_to_delete_unmanaged.is_empty() { + for subkey_name in keys_to_delete_unmanaged { + match unmanaged_key.delete_subkey_all(&subkey_name) { + Ok(_) => tracing::trace!("Successfully deleted subkey: {}", subkey_name), + Err(e) => tracing::error!("Failed to delete subkey {}: {}", subkey_name, e), + } + } + } + Ok(()) +} + +impl VirtualNic { + pub fn new(global_ctx: ArcGlobalCtx) -> Self { + Self { + global_ctx, + ifname: None, + ifcfg: Box::new(IfConfiger {}), + } + } + + async fn create_tun(&mut self) -> Result { + let mut config = Configuration::default(); + config.layer(Layer::L3); + + #[cfg(target_os = "linux")] + { + let dev_name = self.global_ctx.get_flags().dev_name; + if !dev_name.is_empty() { + config.tun_name(format!("{}", dev_name)); + } + } + + #[cfg(any(target_os = "macos"))] + config.platform_config(|config| { + // disable packet information so we can process the header by ourselves, see tun2 impl for more details + config.packet_information(false); + }); + + #[cfg(target_os = "windows")] + { + let dev_name = self.global_ctx.get_flags().dev_name; + + match crate::arch::windows::add_self_to_firewall_allowlist() { + Ok(_) => tracing::info!("add_self_to_firewall_allowlist successful!"), + Err(e) => { + println!("Failed to add Easytier to firewall allowlist, Subnet proxy and KCP proxy may not work properly. error: {}", e); + println!("You can add firewall rules manually, or use --use-smoltcp to run with user-space TCP/IP stack."); + println!(""); + } + } + + match checkreg(&dev_name) { + Ok(_) => tracing::trace!("delete successful!"), + Err(e) => tracing::error!("An error occurred: {}", e), + } + + if !dev_name.is_empty() { + config.tun_name(format!("{}", dev_name)); + } else { + use rand::distributions::Distribution as _; + let c = crate::arch::windows::interface_count()?; + let mut rng = rand::thread_rng(); + let s: String = rand::distributions::Alphanumeric + .sample_iter(&mut rng) + .take(4) + .map(char::from) + .collect::() + .to_lowercase(); + + let random_dev_name = format!("et_{}_{}", c, s); + config.tun_name(random_dev_name.clone()); + + let mut flags = self.global_ctx.get_flags(); + flags.dev_name = random_dev_name.clone(); + self.global_ctx.set_flags(flags); + } + + config.platform_config(|config| { + config.skip_config(true); + config.ring_cap(Some(std::cmp::min( + config.min_ring_cap() * 32, + config.max_ring_cap(), + ))); + }); + } + + config.up(); + + let _g = self.global_ctx.net_ns.guard(); + Ok(tun::create(&config)?) + } + + #[cfg(target_os = "android")] + pub async fn create_dev_for_android( + &mut self, + tun_fd: std::os::fd::RawFd, + ) -> Result, Error> { + println!("tun_fd: {}", tun_fd); + let mut config = Configuration::default(); + config.layer(Layer::L3); + config.raw_fd(tun_fd); + config.close_fd_on_drop(false); + config.up(); + + let dev = tun::create(&config)?; + let dev = AsyncDevice::new(dev)?; + let (a, b) = BiLock::new(dev); + let ft = TunnelWrapper::new( + TunStream::new(a, false), + FramedWriter::new_with_converter( + TunAsyncWrite { l: b }, + TunZCPacketToBytes::new(false), + ), + None, + ); + + self.ifname = Some(format!("tunfd_{}", tun_fd)); + + Ok(Box::new(ft)) + } + + pub async fn create_dev(&mut self) -> Result, Error> { + let dev = self.create_tun().await?; + let ifname = dev.tun_name()?; + self.ifcfg.wait_interface_show(ifname.as_str()).await?; + + let dev = AsyncDevice::new(dev)?; + + let flags = self.global_ctx.config.get_flags(); + let mut mtu_in_config = flags.mtu; + if flags.enable_encryption { + mtu_in_config -= 20; + } + { + // set mtu by ourselves, rust-tun does not handle it correctly on windows + let _g = self.global_ctx.net_ns.guard(); + self.ifcfg + .set_mtu(ifname.as_str(), mtu_in_config as u32) + .await?; + } + + let has_packet_info = cfg!(target_os = "macos"); + let (a, b) = BiLock::new(dev); + let ft = TunnelWrapper::new( + TunStream::new(a, has_packet_info), + FramedWriter::new_with_converter( + TunAsyncWrite { l: b }, + TunZCPacketToBytes::new(has_packet_info), + ), + None, + ); + + self.ifname = Some(ifname.to_owned()); + Ok(Box::new(ft)) + } + + pub fn ifname(&self) -> &str { + self.ifname.as_ref().unwrap().as_str() + } + + pub async fn link_up(&self) -> Result<(), Error> { + let _g = self.global_ctx.net_ns.guard(); + self.ifcfg.set_link_status(self.ifname(), true).await?; + Ok(()) + } + + pub async fn add_route(&self, address: Ipv4Addr, cidr: u8) -> Result<(), Error> { + let _g = self.global_ctx.net_ns.guard(); + self.ifcfg + .add_ipv4_route(self.ifname(), address, cidr) + .await?; + Ok(()) + } + + pub async fn remove_ip(&self, ip: Option) -> Result<(), Error> { + let _g = self.global_ctx.net_ns.guard(); + self.ifcfg.remove_ip(self.ifname(), ip).await?; + Ok(()) + } + + pub async fn add_ip(&self, ip: Ipv4Addr, cidr: i32) -> Result<(), Error> { + let _g = self.global_ctx.net_ns.guard(); + self.ifcfg + .add_ipv4_ip(self.ifname(), ip, cidr as u8) + .await?; + Ok(()) + } + + pub fn get_ifcfg(&self) -> impl IfConfiguerTrait { + IfConfiger {} + } +} + +#[cfg(target_os = "windows")] +pub fn reg_change_catrgory_in_profile(dev_name: &str) -> io::Result<()> { + use winreg::{enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS, RegKey}; + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let profiles_key = hklm.open_subkey_with_flags( + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles", + KEY_ALL_ACCESS, + )?; + + for subkey_name in profiles_key.enum_keys().filter_map(Result::ok) { + let subkey = profiles_key.open_subkey_with_flags(&subkey_name, KEY_ALL_ACCESS)?; + match subkey.get_value::("ProfileName") { + Ok(profile_name) => { + if !dev_name.is_empty() && dev_name == profile_name { + match subkey.set_value("Category", &1u32) { + Ok(_) => tracing::trace!("Successfully set Category in registry"), + Err(e) => tracing::error!("Failed to set Category in registry: {}", e), + } + } + } + Err(e) => { + tracing::error!( + "Failed to read ProfileName for subkey {}: {}", + subkey_name, + e + ); + } + } + } + Ok(()) +} + +pub struct NicCtx { + global_ctx: ArcGlobalCtx, + peer_mgr: Weak, + peer_packet_receiver: Arc>, + + nic: Arc>, + tasks: JoinSet<()>, +} + +impl NicCtx { + pub fn new( + global_ctx: ArcGlobalCtx, + peer_manager: &Arc, + peer_packet_receiver: Arc>, + ) -> Self { + NicCtx { + global_ctx: global_ctx.clone(), + peer_mgr: Arc::downgrade(&peer_manager), + peer_packet_receiver, + nic: Arc::new(Mutex::new(VirtualNic::new(global_ctx))), + tasks: JoinSet::new(), + } + } + + async fn assign_ipv4_to_tun_device(&self, ipv4_addr: cidr::Ipv4Inet) -> Result<(), Error> { + let nic = self.nic.lock().await; + nic.link_up().await?; + nic.remove_ip(None).await?; + nic.add_ip(ipv4_addr.address(), ipv4_addr.network_length() as i32) + .await?; + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + { + nic.add_route(ipv4_addr.first_address(), ipv4_addr.network_length()) + .await?; + } + Ok(()) + } + + async fn do_forward_nic_to_peers_ipv4(ret: ZCPacket, mgr: &PeerManager) { + if let Some(ipv4) = Ipv4Packet::new(ret.payload()) { + if ipv4.get_version() != 4 { + tracing::info!("[USER_PACKET] not ipv4 packet: {:?}", ipv4); + return; + } + let dst_ipv4 = ipv4.get_destination(); + tracing::trace!( + ?ret, + "[USER_PACKET] recv new packet from tun device and forward to peers." + ); + + // TODO: use zero-copy + let send_ret = mgr.send_msg_ipv4(ret, dst_ipv4).await; + if send_ret.is_err() { + tracing::trace!(?send_ret, "[USER_PACKET] send_msg_ipv4 failed") + } + } else { + tracing::warn!(?ret, "[USER_PACKET] not ipv4 packet"); + } + } + + fn do_forward_nic_to_peers( + &mut self, + mut stream: Pin>, + ) -> Result<(), Error> { + // read from nic and write to corresponding tunnel + let Some(mgr) = self.peer_mgr.upgrade() else { + return Err(anyhow::anyhow!("peer manager not available").into()); + }; + self.tasks.spawn(async move { + while let Some(ret) = stream.next().await { + if ret.is_err() { + tracing::error!("read from nic failed: {:?}", ret); + break; + } + Self::do_forward_nic_to_peers_ipv4(ret.unwrap(), mgr.as_ref()).await; + } + panic!("nic stream closed"); + }); + + Ok(()) + } + + fn do_forward_peers_to_nic(&mut self, mut sink: Pin>) { + let channel = self.peer_packet_receiver.clone(); + self.tasks.spawn(async move { + // unlock until coroutine finished + let mut channel = channel.lock().await; + while let Ok(packet) = recv_packet_from_chan(&mut channel).await { + tracing::trace!( + "[USER_PACKET] forward packet from peers to nic. packet: {:?}", + packet + ); + let ret = sink.send(packet).await; + if ret.is_err() { + tracing::error!(?ret, "do_forward_tunnel_to_nic sink error"); + } + } + panic!("peer packet receiver closed"); + }); + } + + async fn run_proxy_cidrs_route_updater(&mut self) -> Result<(), Error> { + let Some(peer_mgr) = self.peer_mgr.upgrade() else { + return Err(anyhow::anyhow!("peer manager not available").into()); + }; + let global_ctx = self.global_ctx.clone(); + let net_ns = self.global_ctx.net_ns.clone(); + let nic = self.nic.lock().await; + let ifcfg = nic.get_ifcfg(); + let ifname = nic.ifname().to_owned(); + + self.tasks.spawn(async move { + let mut cur_proxy_cidrs = vec![]; + loop { + let mut proxy_cidrs = vec![]; + let routes = peer_mgr.list_routes().await; + for r in routes { + for cidr in r.proxy_cidrs { + let Ok(cidr) = cidr.parse::() else { + continue; + }; + proxy_cidrs.push(cidr); + } + } + // add vpn portal cidr to proxy_cidrs + if let Some(vpn_cfg) = global_ctx.config.get_vpn_portal_config() { + proxy_cidrs.push(vpn_cfg.client_cidr); + } + + if let Some(routes) = global_ctx.config.get_routes() { + // if has manual routes, just override entire proxy_cidrs + proxy_cidrs = routes; + } + + // if route is in cur_proxy_cidrs but not in proxy_cidrs, delete it. + for cidr in cur_proxy_cidrs.iter() { + if proxy_cidrs.contains(cidr) { + continue; + } + + let _g = net_ns.guard(); + let ret = ifcfg + .remove_ipv4_route( + ifname.as_str(), + cidr.first_address(), + cidr.network_length(), + ) + .await; + + if ret.is_err() { + tracing::trace!( + cidr = ?cidr, + err = ?ret, + "remove route failed.", + ); + } + } + + for cidr in proxy_cidrs.iter() { + if cur_proxy_cidrs.contains(cidr) { + continue; + } + let _g = net_ns.guard(); + let ret = ifcfg + .add_ipv4_route( + ifname.as_str(), + cidr.first_address(), + cidr.network_length(), + ) + .await; + + if ret.is_err() { + tracing::trace!( + cidr = ?cidr, + err = ?ret, + "add route failed.", + ); + } + } + + cur_proxy_cidrs = proxy_cidrs; + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + }); + + Ok(()) + } + + pub async fn run(&mut self, ipv4_addr: cidr::Ipv4Inet) -> Result<(), Error> { + let tunnel = { + let mut nic = self.nic.lock().await; + match nic.create_dev().await { + Ok(ret) => { + #[cfg(target_os = "windows")] + { + let dev_name = self.global_ctx.get_flags().dev_name; + let _ = reg_change_catrgory_in_profile(&dev_name); + } + + self.global_ctx + .issue_event(GlobalCtxEvent::TunDeviceReady(nic.ifname().to_string())); + ret + } + Err(err) => { + self.global_ctx + .issue_event(GlobalCtxEvent::TunDeviceError(err.to_string())); + return Err(err); + } + } + }; + + let (stream, sink) = tunnel.split(); + + self.do_forward_nic_to_peers(stream)?; + self.do_forward_peers_to_nic(sink); + + self.assign_ipv4_to_tun_device(ipv4_addr).await?; + self.run_proxy_cidrs_route_updater().await?; + + Ok(()) + } + + #[cfg(target_os = "android")] + pub async fn run_for_android(&mut self, tun_fd: std::os::fd::RawFd) -> Result<(), Error> { + let tunnel = { + let mut nic = self.nic.lock().await; + match nic.create_dev_for_android(tun_fd).await { + Ok(ret) => { + self.global_ctx + .issue_event(GlobalCtxEvent::TunDeviceReady(nic.ifname().to_string())); + ret + } + Err(err) => { + self.global_ctx + .issue_event(GlobalCtxEvent::TunDeviceError(err.to_string())); + return Err(err); + } + } + }; + + let (stream, sink) = tunnel.split(); + + self.do_forward_nic_to_peers(stream)?; + self.do_forward_peers_to_nic(sink); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::common::{error::Error, global_ctx::tests::get_mock_global_ctx}; + + use super::VirtualNic; + + async fn run_test_helper() -> Result { + let mut dev = VirtualNic::new(get_mock_global_ctx()); + let _tunnel = dev.create_dev().await?; + + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + dev.link_up().await?; + dev.remove_ip(None).await?; + dev.add_ip("10.144.111.1".parse().unwrap(), 24).await?; + Ok(dev) + } + + #[tokio::test] + async fn tun_test() { + let _dev = run_test_helper().await.unwrap(); + + // let mut stream = nic.pin_recv_stream(); + // while let Some(item) = stream.next().await { + // println!("item: {:?}", item); + // } + + // let framed = dev.into_framed(); + // let (mut s, mut b) = framed.split(); + // loop { + // let tmp = b.next().await.unwrap().unwrap(); + // let tmp = EthernetPacket::new(tmp.get_bytes()); + // println!("ret: {:?}", tmp.unwrap()); + // } + } +} diff --git a/rust/easytier/src/launcher.rs b/rust/easytier/src/launcher.rs new file mode 100644 index 0000000..9e1d6b5 --- /dev/null +++ b/rust/easytier/src/launcher.rs @@ -0,0 +1,618 @@ +use std::{ + collections::VecDeque, + sync::{atomic::AtomicBool, Arc, RwLock}, +}; + +use crate::{ + common::{ + config::{ + gen_default_flags, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader, + VpnPortalConfig, + }, + constants::EASYTIER_VERSION, + global_ctx::{EventBusSubscriber, GlobalCtxEvent}, + stun::StunInfoCollectorTrait, + }, + instance::instance::Instance, + peers::rpc_service::PeerManagerRpcService, + proto::cli::{list_peer_route_pair, PeerInfo, Route}, +}; +use anyhow::Context; +use chrono::{DateTime, Local}; +use tokio::{sync::broadcast, task::JoinSet}; + +pub type MyNodeInfo = crate::proto::web::MyNodeInfo; + +#[derive(serde::Serialize, Clone)] +pub struct Event { + time: DateTime, + event: GlobalCtxEvent, +} + +struct EasyTierData { + events: RwLock>, + my_node_info: RwLock, + routes: RwLock>, + peers: RwLock>, + tun_fd: Arc>>, + tun_dev_name: RwLock, + event_subscriber: RwLock>, + instance_stop_notifier: Arc, +} + +impl Default for EasyTierData { + fn default() -> Self { + let (tx, _) = broadcast::channel(100); + Self { + event_subscriber: RwLock::new(tx), + events: RwLock::new(VecDeque::new()), + my_node_info: RwLock::new(MyNodeInfo::default()), + routes: RwLock::new(Vec::new()), + peers: RwLock::new(Vec::new()), + tun_fd: Arc::new(RwLock::new(None)), + tun_dev_name: RwLock::new(String::new()), + instance_stop_notifier: Arc::new(tokio::sync::Notify::new()), + } + } +} + +pub struct EasyTierLauncher { + instance_alive: Arc, + stop_flag: Arc, + thread_handle: Option>, + running_cfg: String, + fetch_node_info: bool, + + error_msg: Arc>>, + data: Arc, +} + +impl EasyTierLauncher { + pub fn new(fetch_node_info: bool) -> Self { + let instance_alive = Arc::new(AtomicBool::new(false)); + Self { + instance_alive, + thread_handle: None, + error_msg: Arc::new(RwLock::new(None)), + running_cfg: String::new(), + fetch_node_info, + + stop_flag: Arc::new(AtomicBool::new(false)), + data: Arc::new(EasyTierData::default()), + } + } + + async fn handle_easytier_event(event: GlobalCtxEvent, data: &EasyTierData) { + let mut events = data.events.write().unwrap(); + let _ = data.event_subscriber.read().unwrap().send(event.clone()); + events.push_front(Event { + time: chrono::Local::now(), + event: event, + }); + if events.len() > 20 { + events.pop_back(); + } + } + + #[cfg(target_os = "android")] + async fn run_routine_for_android( + instance: &Instance, + data: &EasyTierData, + tasks: &mut JoinSet<()>, + ) { + let global_ctx = instance.get_global_ctx(); + let peer_mgr = instance.get_peer_manager(); + let nic_ctx = instance.get_nic_ctx(); + let peer_packet_receiver = instance.get_peer_packet_receiver(); + let arc_tun_fd = data.tun_fd.clone(); + + tasks.spawn(async move { + let mut old_tun_fd = arc_tun_fd.read().unwrap().clone(); + loop { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + let tun_fd = arc_tun_fd.read().unwrap().clone(); + if tun_fd != old_tun_fd && tun_fd.is_some() { + let res = Instance::setup_nic_ctx_for_android( + nic_ctx.clone(), + global_ctx.clone(), + peer_mgr.clone(), + peer_packet_receiver.clone(), + tun_fd.unwrap(), + ) + .await; + if res.is_ok() { + old_tun_fd = tun_fd; + } + } + } + }); + } + + async fn easytier_routine( + cfg: TomlConfigLoader, + stop_signal: Arc, + data: Arc, + fetch_node_info: bool, + ) -> Result<(), anyhow::Error> { + let mut instance = Instance::new(cfg); + let peer_mgr = instance.get_peer_manager(); + + let mut tasks = JoinSet::new(); + + // Subscribe to global context events + let global_ctx = instance.get_global_ctx(); + let data_c = data.clone(); + tasks.spawn(async move { + let mut receiver = global_ctx.subscribe(); + while let Ok(event) = receiver.recv().await { + Self::handle_easytier_event(event, &data_c).await; + } + }); + + // update my node info + if fetch_node_info { + let data_c = data.clone(); + let global_ctx_c = instance.get_global_ctx(); + let peer_mgr_c = peer_mgr.clone(); + let vpn_portal = instance.get_vpn_portal_inst(); + tasks.spawn(async move { + loop { + // Update TUN Device Name + *data_c.tun_dev_name.write().unwrap() = + global_ctx_c.get_flags().dev_name.clone(); + + let node_info = MyNodeInfo { + virtual_ipv4: global_ctx_c.get_ipv4().map(|ip| ip.into()), + hostname: global_ctx_c.get_hostname(), + version: EASYTIER_VERSION.to_string(), + ips: Some(global_ctx_c.get_ip_collector().collect_ip_addrs().await), + stun_info: Some(global_ctx_c.get_stun_info_collector().get_stun_info()), + listeners: global_ctx_c + .get_running_listeners() + .into_iter() + .map(Into::into) + .collect(), + vpn_portal_cfg: Some( + vpn_portal + .lock() + .await + .dump_client_config(peer_mgr_c.clone()) + .await, + ), + }; + *data_c.my_node_info.write().unwrap() = node_info.clone(); + *data_c.routes.write().unwrap() = peer_mgr_c.list_routes().await; + *data_c.peers.write().unwrap() = PeerManagerRpcService::new(peer_mgr_c.clone()) + .list_peers() + .await; + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + }); + } + + #[cfg(target_os = "android")] + Self::run_routine_for_android(&instance, &data, &mut tasks).await; + + instance.run().await?; + stop_signal.notified().await; + + tasks.abort_all(); + drop(tasks); + + Ok(()) + } + + pub fn start(&mut self, cfg_generator: F) + where + F: FnOnce() -> Result + Send + Sync, + { + let error_msg = self.error_msg.clone(); + let cfg = match cfg_generator() { + Err(e) => { + error_msg.write().unwrap().replace(e.to_string()); + return; + } + Ok(cfg) => cfg, + }; + + self.running_cfg = cfg.dump(); + + let stop_flag = self.stop_flag.clone(); + + let instance_alive = self.instance_alive.clone(); + instance_alive.store(true, std::sync::atomic::Ordering::Relaxed); + + let data = self.data.clone(); + let fetch_node_info = self.fetch_node_info; + + self.thread_handle = Some(std::thread::spawn(move || { + let rt = if cfg.get_flags().multi_thread { + tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build() + } else { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + } + .unwrap(); + + let stop_notifier = Arc::new(tokio::sync::Notify::new()); + + let stop_notifier_clone = stop_notifier.clone(); + rt.spawn(async move { + while !stop_flag.load(std::sync::atomic::Ordering::Relaxed) { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + stop_notifier_clone.notify_one(); + }); + + let notifier = data.instance_stop_notifier.clone(); + let ret = rt.block_on(Self::easytier_routine( + cfg, + stop_notifier.clone(), + data, + fetch_node_info, + )); + if let Err(e) = ret { + error_msg.write().unwrap().replace(format!("{:?}", e)); + } + instance_alive.store(false, std::sync::atomic::Ordering::Relaxed); + notifier.notify_one(); + })); + } + + pub fn error_msg(&self) -> Option { + self.error_msg.read().unwrap().clone() + } + + pub fn running(&self) -> bool { + self.instance_alive + .load(std::sync::atomic::Ordering::Relaxed) + } + + pub fn get_dev_name(&self) -> String { + self.data.tun_dev_name.read().unwrap().clone() + } + + pub fn get_events(&self) -> Vec { + let events = self.data.events.read().unwrap(); + events.iter().cloned().collect() + } + + pub fn get_node_info(&self) -> MyNodeInfo { + self.data.my_node_info.read().unwrap().clone() + } + + pub fn get_routes(&self) -> Vec { + self.data.routes.read().unwrap().clone() + } + + pub fn get_peers(&self) -> Vec { + self.data.peers.read().unwrap().clone() + } +} + +impl Drop for EasyTierLauncher { + fn drop(&mut self) { + self.stop_flag + .store(true, std::sync::atomic::Ordering::Relaxed); + if let Some(handle) = self.thread_handle.take() { + if let Err(e) = handle.join() { + println!("Error when joining thread: {:?}", e); + } + } + } +} + +pub type NetworkInstanceRunningInfo = crate::proto::web::NetworkInstanceRunningInfo; + +pub struct NetworkInstance { + config: TomlConfigLoader, + launcher: Option, + + fetch_node_info: bool, +} + +impl NetworkInstance { + pub fn new(config: TomlConfigLoader) -> Self { + Self { + config, + launcher: None, + fetch_node_info: true, + } + } + + pub fn set_fetch_node_info(mut self, fetch_node_info: bool) -> Self { + self.fetch_node_info = fetch_node_info; + self + } + + pub fn is_easytier_running(&self) -> bool { + self.launcher.is_some() && self.launcher.as_ref().unwrap().running() + } + + pub fn get_running_info(&self) -> Option { + if self.launcher.is_none() { + return None; + } + + let launcher = self.launcher.as_ref().unwrap(); + + let peers = launcher.get_peers(); + let routes = launcher.get_routes(); + let peer_route_pairs = list_peer_route_pair(peers.clone(), routes.clone()); + + Some(NetworkInstanceRunningInfo { + dev_name: launcher.get_dev_name(), + my_node_info: Some(launcher.get_node_info()), + events: launcher + .get_events() + .iter() + .map(|e| serde_json::to_string(e).unwrap()) + .collect(), + routes, + peers, + peer_route_pairs, + running: launcher.running(), + error_msg: launcher.error_msg(), + }) + } + + pub fn set_tun_fd(&mut self, tun_fd: i32) { + if let Some(launcher) = self.launcher.as_ref() { + launcher.data.tun_fd.write().unwrap().replace(tun_fd); + } + } + + pub fn start(&mut self) -> Result { + if self.is_easytier_running() { + return Ok(self.subscribe_event().unwrap()); + } + + let launcher = EasyTierLauncher::new(self.fetch_node_info); + self.launcher = Some(launcher); + let ev = self.subscribe_event().unwrap(); + + self.launcher + .as_mut() + .unwrap() + .start(|| Ok(self.config.clone())); + + Ok(ev) + } + + fn subscribe_event(&self) -> Option> { + if let Some(launcher) = self.launcher.as_ref() { + Some(launcher.data.event_subscriber.read().unwrap().subscribe()) + } else { + None + } + } + + pub async fn wait(&self) -> Option { + if let Some(launcher) = self.launcher.as_ref() { + launcher.data.instance_stop_notifier.notified().await; + launcher.error_msg.read().unwrap().clone() + } else { + None + } + } +} + +pub type NetworkingMethod = crate::proto::web::NetworkingMethod; +pub type NetworkConfig = crate::proto::web::NetworkConfig; + +impl NetworkConfig { + pub fn gen_config(&self) -> Result { + let cfg = TomlConfigLoader::default(); + cfg.set_id( + self.instance_id + .clone() + .unwrap_or(uuid::Uuid::new_v4().to_string()) + .parse() + .with_context(|| format!("failed to parse instance id: {:?}", self.instance_id))?, + ); + cfg.set_hostname(self.hostname.clone()); + cfg.set_dhcp(self.dhcp.unwrap_or_default()); + cfg.set_inst_name(self.network_name.clone().unwrap_or_default()); + cfg.set_network_identity(NetworkIdentity::new( + self.network_name.clone().unwrap_or_default(), + self.network_secret.clone().unwrap_or_default(), + )); + + if !cfg.get_dhcp() { + let virtual_ipv4 = self.virtual_ipv4.clone().unwrap_or_default(); + if virtual_ipv4.len() > 0 { + let ip = format!("{}/{}", virtual_ipv4, self.network_length.unwrap_or(24)) + .parse() + .with_context(|| { + format!( + "failed to parse ipv4 inet address: {}, {:?}", + virtual_ipv4, self.network_length + ) + })?; + cfg.set_ipv4(Some(ip)); + } + } + + match NetworkingMethod::try_from(self.networking_method.unwrap_or_default()) + .unwrap_or_default() + { + NetworkingMethod::PublicServer => { + let public_server_url = self.public_server_url.clone().unwrap_or_default(); + cfg.set_peers(vec![PeerConfig { + uri: public_server_url.parse().with_context(|| { + format!("failed to parse public server uri: {}", public_server_url) + })?, + }]); + } + NetworkingMethod::Manual => { + let mut peers = vec![]; + for peer_url in self.peer_urls.iter() { + if peer_url.is_empty() { + continue; + } + peers.push(PeerConfig { + uri: peer_url + .parse() + .with_context(|| format!("failed to parse peer uri: {}", peer_url))?, + }); + } + + cfg.set_peers(peers); + } + NetworkingMethod::Standalone => {} + } + + let mut listener_urls = vec![]; + for listener_url in self.listener_urls.iter() { + if listener_url.is_empty() { + continue; + } + listener_urls.push( + listener_url + .parse() + .with_context(|| format!("failed to parse listener uri: {}", listener_url))?, + ); + } + cfg.set_listeners(listener_urls); + + for n in self.proxy_cidrs.iter() { + cfg.add_proxy_cidr( + n.parse() + .with_context(|| format!("failed to parse proxy network: {}", n))?, + ); + } + + cfg.set_rpc_portal( + format!("0.0.0.0:{}", self.rpc_port.unwrap_or_default()) + .parse() + .with_context(|| format!("failed to parse rpc portal port: {:?}", self.rpc_port))?, + ); + + if self.enable_vpn_portal.unwrap_or_default() { + let cidr = format!( + "{}/{}", + self.vpn_portal_client_network_addr + .clone() + .unwrap_or_default(), + self.vpn_portal_client_network_len.unwrap_or(24) + ); + cfg.set_vpn_portal_config(VpnPortalConfig { + client_cidr: cidr + .parse() + .with_context(|| format!("failed to parse vpn portal client cidr: {}", cidr))?, + wireguard_listen: format!( + "0.0.0.0:{}", + self.vpn_portal_listen_port.unwrap_or_default() + ) + .parse() + .with_context(|| { + format!( + "failed to parse vpn portal wireguard listen port. {:?}", + self.vpn_portal_listen_port + ) + })?, + }); + } + + if self.enable_manual_routes.unwrap_or_default() { + let mut routes = Vec::::with_capacity(self.routes.len()); + for route in self.routes.iter() { + routes.push( + route.parse() + .with_context(|| format!("failed to parse route: {}", route))?, + ); + } + cfg.set_routes(Some(routes)); + } + + if self.exit_nodes.len() > 0 { + let mut exit_nodes = Vec::::with_capacity(self.exit_nodes.len()); + for node in self.exit_nodes.iter() { + exit_nodes.push( + node.parse() + .with_context(|| format!("failed to parse exit node: {}", node))?, + ); + } + cfg.set_exit_nodes(exit_nodes); + } + + if self.enable_socks5.unwrap_or_default() { + if let Some(socks5_port) = self.socks5_port { + cfg.set_socks5_portal(Some( + format!("socks5://0.0.0.0:{}", socks5_port) + .parse() + .unwrap(), + )); + } + } + + let mut flags = gen_default_flags(); + if let Some(latency_first) = self.latency_first { + flags.latency_first = latency_first; + } + + if let Some(dev_name) = self.dev_name.clone() { + flags.dev_name = dev_name; + } + + if let Some(use_smoltcp) = self.use_smoltcp { + flags.use_smoltcp = use_smoltcp; + } + + if let Some(enable_kcp_proxy) = self.enable_kcp_proxy { + flags.enable_kcp_proxy = enable_kcp_proxy; + } + + if let Some(disable_kcp_input) = self.disable_kcp_input { + flags.disable_kcp_input = disable_kcp_input; + } + + if let Some(disable_p2p) = self.disable_p2p { + flags.disable_p2p = disable_p2p; + } + + if let Some(bind_device) = self.bind_device { + flags.bind_device = bind_device; + } + + if let Some(no_tun) = self.no_tun { + flags.no_tun = no_tun; + } + + if let Some(enable_exit_node) = self.enable_exit_node { + flags.enable_exit_node = enable_exit_node; + } + + if let Some(relay_all_peer_rpc) = self.relay_all_peer_rpc { + flags.relay_all_peer_rpc = relay_all_peer_rpc; + } + + if let Some(multi_thread) = self.multi_thread { + flags.multi_thread = multi_thread; + } + + if let Some(proxy_forward_by_system) = self.proxy_forward_by_system { + flags.proxy_forward_by_system = proxy_forward_by_system; + } + + if let Some(disable_encryption) = self.disable_encryption { + flags.enable_encryption = !disable_encryption; + } + + if self.enable_relay_network_whitelist.unwrap_or_default() { + if self.relay_network_whitelist.len() > 0 { + flags.relay_network_whitelist = self.relay_network_whitelist.join(" ") + } else { + flags.relay_network_whitelist = "".to_string() + } + + } + + cfg.set_flags(flags); + Ok(cfg) + } +} diff --git a/rust/easytier/src/lib.rs b/rust/easytier/src/lib.rs new file mode 100644 index 0000000..23fa00b --- /dev/null +++ b/rust/easytier/src/lib.rs @@ -0,0 +1,22 @@ +#![allow(dead_code)] + +mod arch; +mod gateway; +mod instance; +mod peer_center; +mod vpn_portal; + +pub mod common; +pub mod connector; +pub mod launcher; +pub mod peers; +pub mod proto; +pub mod tunnel; +pub mod utils; +pub mod web_client; + +#[cfg(test)] +mod tests; + +pub const VERSION: &str = common::constants::EASYTIER_VERSION; +rust_i18n::i18n!("locales", fallback = "en"); diff --git a/rust/easytier/src/peer_center/instance.rs b/rust/easytier/src/peer_center/instance.rs new file mode 100644 index 0000000..0537c7b --- /dev/null +++ b/rust/easytier/src/peer_center/instance.rs @@ -0,0 +1,490 @@ +use std::{ + collections::BTreeSet, + sync::Arc, + time::{Duration, Instant}, +}; + +use crossbeam::atomic::AtomicCell; +use futures::Future; +use std::sync::RwLock; +use tokio::sync::Mutex; +use tokio::task::JoinSet; +use tracing::Instrument; + +use crate::{ + common::PeerId, + peers::{ + peer_manager::PeerManager, + route_trait::{RouteCostCalculator, RouteCostCalculatorInterface}, + rpc_service::PeerManagerRpcService, + }, + proto::{ + peer_rpc::{ + GetGlobalPeerMapRequest, GetGlobalPeerMapResponse, GlobalPeerMap, PeerCenterRpc, + PeerCenterRpcClientFactory, PeerCenterRpcServer, PeerInfoForGlobalMap, + ReportPeersRequest, ReportPeersResponse, + }, + rpc_types::{self, controller::BaseController}, + }, +}; + +use super::{server::PeerCenterServer, Digest, Error}; + +struct PeerCenterBase { + peer_mgr: Arc, + tasks: Arc>>, + lock: Arc>, +} + +// static SERVICE_ID: u32 = 5; for compatibility with the original code +static SERVICE_ID: u32 = 50; + +struct PeridicJobCtx { + peer_mgr: Arc, + center_peer: AtomicCell, + job_ctx: T, +} + +impl PeerCenterBase { + pub async fn init(&self) -> Result<(), Error> { + self.peer_mgr + .get_peer_rpc_mgr() + .rpc_server() + .registry() + .register( + PeerCenterRpcServer::new(PeerCenterServer::new(self.peer_mgr.my_peer_id())), + &self.peer_mgr.get_global_ctx().get_network_name(), + ); + Ok(()) + } + + async fn select_center_peer(peer_mgr: &Arc) -> Option { + let peers = peer_mgr.list_routes().await; + if peers.is_empty() { + return None; + } + // find peer with alphabetical smallest id. + let mut min_peer = peer_mgr.my_peer_id(); + for peer in peers + .iter() + .filter(|r| r.feature_flag.map(|r| !r.is_public_server).unwrap_or(true)) + { + let peer_id = peer.peer_id; + if peer_id < min_peer { + min_peer = peer_id; + } + } + Some(min_peer) + } + + async fn init_periodic_job< + T: Send + Sync + 'static + Clone, + Fut: Future> + Send + 'static, + >( + &self, + job_ctx: T, + job_fn: (impl Fn( + Box + Send>, + Arc>, + ) -> Fut + + Send + + Sync + + 'static), + ) -> () { + let my_peer_id = self.peer_mgr.my_peer_id(); + let peer_mgr = self.peer_mgr.clone(); + let lock = self.lock.clone(); + self.tasks.lock().await.spawn( + async move { + let ctx = Arc::new(PeridicJobCtx { + peer_mgr: peer_mgr.clone(), + center_peer: AtomicCell::new(PeerId::default()), + job_ctx, + }); + loop { + let Some(center_peer) = Self::select_center_peer(&peer_mgr).await else { + tracing::trace!("no center peer found, sleep 1 second"); + tokio::time::sleep(Duration::from_secs(1)).await; + continue; + }; + ctx.center_peer.store(center_peer.clone()); + tracing::trace!(?center_peer, "run periodic job"); + let rpc_mgr = peer_mgr.get_peer_rpc_mgr(); + let _g = lock.lock().await; + let stub = rpc_mgr + .rpc_client() + .scoped_client::>( + my_peer_id, + center_peer, + peer_mgr.get_global_ctx().get_network_name(), + ); + let ret = job_fn(stub, ctx.clone()).await; + drop(_g); + + let Ok(sleep_time_ms) = ret else { + tracing::error!("periodic job to center server rpc failed: {:?}", ret); + tokio::time::sleep(Duration::from_secs(3)).await; + continue; + }; + + if sleep_time_ms > 0 { + tokio::time::sleep(Duration::from_millis(sleep_time_ms as u64)).await; + } + } + } + .instrument(tracing::info_span!("periodic_job", ?my_peer_id)), + ); + } + + pub fn new(peer_mgr: Arc) -> Self { + PeerCenterBase { + peer_mgr, + tasks: Arc::new(Mutex::new(JoinSet::new())), + lock: Arc::new(Mutex::new(())), + } + } +} + +#[derive(Clone)] +pub struct PeerCenterInstanceService { + global_peer_map: Arc>, + global_peer_map_digest: Arc>, +} + +#[async_trait::async_trait] +impl PeerCenterRpc for PeerCenterInstanceService { + type Controller = BaseController; + + async fn get_global_peer_map( + &self, + _: BaseController, + _: GetGlobalPeerMapRequest, + ) -> Result { + let global_peer_map = self.global_peer_map.read().unwrap(); + Ok(GetGlobalPeerMapResponse { + global_peer_map: global_peer_map.map.clone(), + digest: Some(self.global_peer_map_digest.load()), + }) + } + + async fn report_peers( + &self, + _: BaseController, + _req: ReportPeersRequest, + ) -> Result { + Err(anyhow::anyhow!("not implemented").into()) + } +} + +pub struct PeerCenterInstance { + peer_mgr: Arc, + + client: Arc, + global_peer_map: Arc>, + global_peer_map_digest: Arc>, + global_peer_map_update_time: Arc>, +} + +impl PeerCenterInstance { + pub fn new(peer_mgr: Arc) -> Self { + PeerCenterInstance { + peer_mgr: peer_mgr.clone(), + client: Arc::new(PeerCenterBase::new(peer_mgr.clone())), + global_peer_map: Arc::new(RwLock::new(GlobalPeerMap::default())), + global_peer_map_digest: Arc::new(AtomicCell::new(Digest::default())), + global_peer_map_update_time: Arc::new(AtomicCell::new(Instant::now())), + } + } + + pub async fn init(&self) { + self.client.init().await.unwrap(); + self.init_get_global_info_job().await; + self.init_report_peers_job().await; + } + + async fn init_get_global_info_job(&self) { + struct Ctx { + global_peer_map: Arc>, + global_peer_map_digest: Arc>, + global_peer_map_update_time: Arc>, + } + + let ctx = Arc::new(Ctx { + global_peer_map: self.global_peer_map.clone(), + global_peer_map_digest: self.global_peer_map_digest.clone(), + global_peer_map_update_time: self.global_peer_map_update_time.clone(), + }); + + self.client + .init_periodic_job(ctx, |client, ctx| async move { + if ctx + .job_ctx + .global_peer_map_update_time + .load() + .elapsed() + .as_secs() + > 120 + { + ctx.job_ctx.global_peer_map_digest.store(Digest::default()); + } + + let ret = client + .get_global_peer_map( + BaseController::default(), + GetGlobalPeerMapRequest { + digest: ctx.job_ctx.global_peer_map_digest.load(), + }, + ) + .await; + + let Ok(resp) = ret else { + tracing::error!( + "get global info from center server got error result: {:?}", + ret + ); + return Ok(10000); + }; + + if resp == GetGlobalPeerMapResponse::default() { + // digest match, no need to update + return Ok(15000); + } + + tracing::info!( + "get global info from center server: {:?}, digest: {:?}", + resp.global_peer_map, + resp.digest + ); + + *ctx.job_ctx.global_peer_map.write().unwrap() = GlobalPeerMap { + map: resp.global_peer_map, + }; + ctx.job_ctx + .global_peer_map_digest + .store(resp.digest.unwrap_or_default()); + ctx.job_ctx + .global_peer_map_update_time + .store(Instant::now()); + + Ok(15000) + }) + .await; + } + + async fn init_report_peers_job(&self) { + struct Ctx { + service: PeerManagerRpcService, + + last_report_peers: Mutex>, + + last_center_peer: AtomicCell, + last_report_time: AtomicCell, + } + let ctx = Arc::new(Ctx { + service: PeerManagerRpcService::new(self.peer_mgr.clone()), + last_report_peers: Mutex::new(BTreeSet::new()), + last_center_peer: AtomicCell::new(PeerId::default()), + last_report_time: AtomicCell::new(Instant::now()), + }); + + self.client + .init_periodic_job(ctx, |client, ctx| async move { + let my_node_id = ctx.peer_mgr.my_peer_id(); + let peers: PeerInfoForGlobalMap = ctx.job_ctx.service.list_peers().await.into(); + let peer_list = peers.direct_peers.keys().map(|k| *k).collect(); + let job_ctx = &ctx.job_ctx; + + // only report when: + // 1. center peer changed + // 2. last report time is more than 60 seconds + // 3. peers changed + if ctx.center_peer.load() == ctx.job_ctx.last_center_peer.load() + && job_ctx.last_report_time.load().elapsed().as_secs() < 60 + && *job_ctx.last_report_peers.lock().await == peer_list + { + return Ok(5000); + } + + let ret = client + .report_peers( + BaseController::default(), + ReportPeersRequest { + my_peer_id: my_node_id, + peer_infos: Some(peers), + }, + ) + .await; + + if ret.is_ok() { + ctx.job_ctx.last_center_peer.store(ctx.center_peer.load()); + *ctx.job_ctx.last_report_peers.lock().await = peer_list; + ctx.job_ctx.last_report_time.store(Instant::now()); + } else { + tracing::error!("report peers to center server got error result: {:?}", ret); + } + + Ok(5000) + }) + .await; + } + + pub fn get_rpc_service(&self) -> PeerCenterInstanceService { + PeerCenterInstanceService { + global_peer_map: self.global_peer_map.clone(), + global_peer_map_digest: self.global_peer_map_digest.clone(), + } + } + + pub fn get_cost_calculator(&self) -> RouteCostCalculator { + struct RouteCostCalculatorImpl { + global_peer_map: Arc>, + + global_peer_map_clone: GlobalPeerMap, + + last_update_time: AtomicCell, + global_peer_map_update_time: Arc>, + } + + impl RouteCostCalculatorImpl { + fn directed_cost(&self, src: PeerId, dst: PeerId) -> Option { + self.global_peer_map_clone + .map + .get(&src) + .and_then(|src_peer_info| src_peer_info.direct_peers.get(&dst)) + .and_then(|info| Some(info.latency_ms)) + } + } + + impl RouteCostCalculatorInterface for RouteCostCalculatorImpl { + fn calculate_cost(&self, src: PeerId, dst: PeerId) -> i32 { + if let Some(cost) = self.directed_cost(src, dst) { + return cost; + } + self.directed_cost(dst, src).unwrap_or(100) + } + + fn begin_update(&mut self) { + let global_peer_map = self.global_peer_map.read().unwrap(); + self.global_peer_map_clone = global_peer_map.clone(); + } + + fn end_update(&mut self) { + self.last_update_time + .store(self.global_peer_map_update_time.load()); + } + + fn need_update(&self) -> bool { + self.last_update_time.load() < self.global_peer_map_update_time.load() + } + } + + Box::new(RouteCostCalculatorImpl { + global_peer_map: self.global_peer_map.clone(), + global_peer_map_clone: GlobalPeerMap::default(), + last_update_time: AtomicCell::new( + self.global_peer_map_update_time.load() - Duration::from_secs(1), + ), + global_peer_map_update_time: self.global_peer_map_update_time.clone(), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + peer_center::server::get_global_data, + peers::tests::{connect_peer_manager, create_mock_peer_manager, wait_route_appear}, + tunnel::common::tests::wait_for_condition, + }; + + use super::*; + + #[tokio::test] + async fn test_peer_center_instance() { + let peer_mgr_a = create_mock_peer_manager().await; + let peer_mgr_b = create_mock_peer_manager().await; + let peer_mgr_c = create_mock_peer_manager().await; + + let peer_center_a = PeerCenterInstance::new(peer_mgr_a.clone()); + let peer_center_b = PeerCenterInstance::new(peer_mgr_b.clone()); + let peer_center_c = PeerCenterInstance::new(peer_mgr_c.clone()); + + let peer_centers = vec![&peer_center_a, &peer_center_b, &peer_center_c]; + for pc in peer_centers.iter() { + pc.init().await; + } + + connect_peer_manager(peer_mgr_a.clone(), peer_mgr_b.clone()).await; + connect_peer_manager(peer_mgr_b.clone(), peer_mgr_c.clone()).await; + + wait_route_appear(peer_mgr_a.clone(), peer_mgr_c.clone()) + .await + .unwrap(); + + let center_peer = PeerCenterBase::select_center_peer(&peer_mgr_a) + .await + .unwrap(); + let center_data = get_global_data(center_peer); + + // wait center_data has 3 records for 10 seconds + wait_for_condition( + || async { + if center_data.global_peer_map.len() == 4 { + println!("center data {:#?}", center_data.global_peer_map); + true + } else { + false + } + }, + Duration::from_secs(20), + ) + .await; + + let mut digest = None; + for pc in peer_centers.iter() { + let rpc_service = pc.get_rpc_service(); + wait_for_condition( + || async { rpc_service.global_peer_map.read().unwrap().map.len() == 3 }, + Duration::from_secs(20), + ) + .await; + + println!("rpc service ready, {:#?}", rpc_service.global_peer_map); + + if digest.is_none() { + digest = Some(rpc_service.global_peer_map_digest.load()); + } else { + let v = rpc_service.global_peer_map_digest.load(); + assert_eq!(digest.unwrap(), v); + } + + let mut route_cost = pc.get_cost_calculator(); + assert!(route_cost.need_update()); + + route_cost.begin_update(); + assert!( + route_cost.calculate_cost(peer_mgr_a.my_peer_id(), peer_mgr_b.my_peer_id()) < 30 + ); + assert!( + route_cost.calculate_cost(peer_mgr_b.my_peer_id(), peer_mgr_a.my_peer_id()) < 30 + ); + assert!( + route_cost.calculate_cost(peer_mgr_b.my_peer_id(), peer_mgr_c.my_peer_id()) < 30 + ); + assert!( + route_cost.calculate_cost(peer_mgr_c.my_peer_id(), peer_mgr_b.my_peer_id()) < 30 + ); + assert!( + route_cost.calculate_cost(peer_mgr_c.my_peer_id(), peer_mgr_a.my_peer_id()) > 50 + ); + assert!( + route_cost.calculate_cost(peer_mgr_a.my_peer_id(), peer_mgr_c.my_peer_id()) > 50 + ); + route_cost.end_update(); + assert!(!route_cost.need_update()); + } + + let global_digest = get_global_data(center_peer).digest.load(); + assert_eq!(digest.as_ref().unwrap(), &global_digest); + } +} diff --git a/rust/easytier/src/peer_center/mod.rs b/rust/easytier/src/peer_center/mod.rs new file mode 100644 index 0000000..71d83cd --- /dev/null +++ b/rust/easytier/src/peer_center/mod.rs @@ -0,0 +1,50 @@ +// peer_center is used to collect peer info into one peer node. +// the center node is selected with the following rules: +// 1. has smallest peer id +// 2. TODO: has allow_to_be_center peer feature +// peer center is not guaranteed to be stable and can be changed when peer enter or leave. +// it's used to reduce the cost to exchange infos between peers. + +use std::collections::BTreeMap; + +use crate::proto::cli::PeerInfo; +use crate::proto::peer_rpc::{DirectConnectedPeerInfo, PeerInfoForGlobalMap}; + +pub mod instance; +mod server; + +#[derive(thiserror::Error, Debug, serde::Deserialize, serde::Serialize)] +pub enum Error { + #[error("Digest not match, need provide full peer info to center server.")] + DigestMismatch, + #[error("Not center server")] + NotCenterServer, +} + +pub type Digest = u64; + +impl From> for PeerInfoForGlobalMap { + fn from(peers: Vec) -> Self { + let mut peer_map = BTreeMap::new(); + for peer in peers { + let Some(min_lat) = peer + .conns + .iter() + .map(|conn| conn.stats.as_ref().unwrap().latency_us) + .min() + else { + continue; + }; + + let dp_info = DirectConnectedPeerInfo { + latency_ms: std::cmp::max(1, (min_lat as u32 / 1000) as i32), + }; + + // sort conn info so hash result is stable + peer_map.insert(peer.peer_id, dp_info); + } + PeerInfoForGlobalMap { + direct_peers: peer_map, + } + } +} diff --git a/rust/easytier/src/peer_center/server.rs b/rust/easytier/src/peer_center/server.rs new file mode 100644 index 0000000..9d98f4e --- /dev/null +++ b/rust/easytier/src/peer_center/server.rs @@ -0,0 +1,173 @@ +use std::{ + collections::BinaryHeap, + hash::{Hash, Hasher}, + sync::Arc, +}; + +use crossbeam::atomic::AtomicCell; +use dashmap::DashMap; +use once_cell::sync::Lazy; +use tokio::task::JoinSet; + +use crate::{ + common::PeerId, + proto::{ + peer_rpc::{ + DirectConnectedPeerInfo, GetGlobalPeerMapRequest, GetGlobalPeerMapResponse, + GlobalPeerMap, PeerCenterRpc, PeerInfoForGlobalMap, ReportPeersRequest, + ReportPeersResponse, + }, + rpc_types::{self, controller::BaseController}, + }, +}; + +use super::Digest; + +#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)] +pub(crate) struct SrcDstPeerPair { + src: PeerId, + dst: PeerId, +} + +#[derive(Debug, Clone)] +pub(crate) struct PeerCenterInfoEntry { + info: DirectConnectedPeerInfo, + update_time: std::time::Instant, +} + +#[derive(Default)] +pub(crate) struct PeerCenterServerGlobalData { + pub(crate) global_peer_map: DashMap, + pub(crate) peer_report_time: DashMap, + pub(crate) digest: AtomicCell, +} + +// a global unique instance for PeerCenterServer +pub(crate) static GLOBAL_DATA: Lazy>> = + Lazy::new(DashMap::new); + +pub(crate) fn get_global_data(node_id: PeerId) -> Arc { + GLOBAL_DATA + .entry(node_id) + .or_insert_with(|| Arc::new(PeerCenterServerGlobalData::default())) + .value() + .clone() +} + +#[derive(Clone, Debug)] +pub struct PeerCenterServer { + // every peer has its own server, so use per-struct dash map is ok. + my_node_id: PeerId, + tasks: Arc>, +} + +impl PeerCenterServer { + pub fn new(my_node_id: PeerId) -> Self { + let mut tasks = JoinSet::new(); + tasks.spawn(async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + PeerCenterServer::clean_outdated_peer(my_node_id).await; + } + }); + + PeerCenterServer { + my_node_id, + tasks: Arc::new(tasks), + } + } + + async fn clean_outdated_peer(my_node_id: PeerId) { + let data = get_global_data(my_node_id); + data.peer_report_time.retain(|_, v| { + std::time::Instant::now().duration_since(*v) < std::time::Duration::from_secs(180) + }); + data.global_peer_map.retain(|_, v| { + std::time::Instant::now().duration_since(v.update_time) + < std::time::Duration::from_secs(180) + }); + } + + fn calc_global_digest(my_node_id: PeerId) -> Digest { + let data = get_global_data(my_node_id); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + data.global_peer_map + .iter() + .map(|v| v.key().clone()) + .collect::>() + .into_sorted_vec() + .into_iter() + .for_each(|v| v.hash(&mut hasher)); + hasher.finish() + } +} + +#[async_trait::async_trait] +impl PeerCenterRpc for PeerCenterServer { + type Controller = BaseController; + + #[tracing::instrument()] + async fn report_peers( + &self, + _: BaseController, + req: ReportPeersRequest, + ) -> Result { + let my_peer_id = req.my_peer_id; + let peers = req.peer_infos.unwrap_or_default(); + + tracing::debug!("receive report_peers"); + + let data = get_global_data(self.my_node_id); + data.peer_report_time + .insert(my_peer_id, std::time::Instant::now()); + + for (peer_id, peer_info) in peers.direct_peers { + let pair = SrcDstPeerPair { + src: my_peer_id, + dst: peer_id, + }; + let entry = PeerCenterInfoEntry { + info: peer_info, + update_time: std::time::Instant::now(), + }; + data.global_peer_map.insert(pair, entry); + } + + data.digest + .store(PeerCenterServer::calc_global_digest(self.my_node_id)); + + Ok(ReportPeersResponse::default()) + } + + #[tracing::instrument()] + async fn get_global_peer_map( + &self, + _: BaseController, + req: GetGlobalPeerMapRequest, + ) -> Result { + let digest = req.digest; + + let data = get_global_data(self.my_node_id); + if digest == data.digest.load() && digest != 0 { + return Ok(GetGlobalPeerMapResponse::default()); + } + + let mut global_peer_map = GlobalPeerMap::default(); + for item in data.global_peer_map.iter() { + let (pair, entry) = item.pair(); + global_peer_map + .map + .entry(pair.src) + .or_insert_with(|| PeerInfoForGlobalMap { + direct_peers: Default::default(), + }) + .direct_peers + .insert(pair.dst, entry.info.clone()); + } + + Ok(GetGlobalPeerMapResponse { + global_peer_map: global_peer_map.map, + digest: Some(data.digest.load()), + }) + } +} diff --git a/rust/easytier/src/peers/encrypt/aes_gcm.rs b/rust/easytier/src/peers/encrypt/aes_gcm.rs new file mode 100644 index 0000000..ab39698 --- /dev/null +++ b/rust/easytier/src/peers/encrypt/aes_gcm.rs @@ -0,0 +1,146 @@ +use aes_gcm::aead::consts::{U12, U16}; +use aes_gcm::aead::generic_array::GenericArray; +use aes_gcm::{AeadCore, AeadInPlace, Aes128Gcm, Aes256Gcm, Key, KeyInit, Nonce, Tag}; +use rand::rngs::OsRng; +use zerocopy::{AsBytes, FromBytes}; + +use crate::tunnel::packet_def::{AesGcmTail, ZCPacket, AES_GCM_ENCRYPTION_RESERVED}; + +use super::{Encryptor, Error}; + +#[derive(Clone)] +pub struct AesGcmCipher { + pub(crate) cipher: AesGcmEnum, +} + +#[derive(Clone)] +pub enum AesGcmEnum { + AES128GCM(Aes128Gcm), + AES256GCM(Aes256Gcm), +} + +impl AesGcmCipher { + pub fn new_128(key: [u8; 16]) -> Self { + let key: &Key = &key.into(); + Self { + cipher: AesGcmEnum::AES128GCM(Aes128Gcm::new(key)), + } + } + pub fn new_256(key: [u8; 32]) -> Self { + let key: &Key = &key.into(); + Self { + cipher: AesGcmEnum::AES256GCM(Aes256Gcm::new(key)), + } + } +} + +impl Encryptor for AesGcmCipher { + fn decrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> { + let pm_header = zc_packet.peer_manager_header().unwrap(); + if !pm_header.is_encrypted() { + return Ok(()); + } + + let payload_len = zc_packet.payload().len(); + if payload_len < AES_GCM_ENCRYPTION_RESERVED { + return Err(Error::PacketTooShort(zc_packet.payload().len())); + } + + let text_len = payload_len - AES_GCM_ENCRYPTION_RESERVED; + + let aes_tail = AesGcmTail::ref_from_suffix(zc_packet.payload()) + .unwrap() + .clone(); + let nonce: &GenericArray = Nonce::from_slice(&aes_tail.nonce); + + let tag: GenericArray = Tag::clone_from_slice(aes_tail.tag.as_slice()); + let rs = match &self.cipher { + AesGcmEnum::AES128GCM(aes_gcm) => aes_gcm.decrypt_in_place_detached( + nonce, + &[], + &mut zc_packet.mut_payload()[..text_len], + &tag, + ), + AesGcmEnum::AES256GCM(aes_gcm) => aes_gcm.decrypt_in_place_detached( + nonce, + &[], + &mut zc_packet.mut_payload()[..text_len], + &tag, + ), + }; + + if let Err(e) = rs { + println!("error: {:?}", e.to_string()); + return Err(Error::DecryptionFailed); + } + + let pm_header = zc_packet.mut_peer_manager_header().unwrap(); + pm_header.set_encrypted(false); + let old_len = zc_packet.buf_len(); + zc_packet + .mut_inner() + .truncate(old_len - AES_GCM_ENCRYPTION_RESERVED); + return Ok(()); + } + + fn encrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> { + let pm_header = zc_packet.peer_manager_header().unwrap(); + if pm_header.is_encrypted() { + tracing::warn!(?zc_packet, "packet is already encrypted"); + return Ok(()); + } + + let mut tail = AesGcmTail::default(); + let rs = match &self.cipher { + AesGcmEnum::AES128GCM(aes_gcm) => { + let nonce = Aes128Gcm::generate_nonce(&mut OsRng); + tail.nonce.copy_from_slice(nonce.as_slice()); + aes_gcm.encrypt_in_place_detached(&nonce, &[], zc_packet.mut_payload()) + } + AesGcmEnum::AES256GCM(aes_gcm) => { + let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + tail.nonce.copy_from_slice(nonce.as_slice()); + aes_gcm.encrypt_in_place_detached(&nonce, &[], zc_packet.mut_payload()) + } + }; + + return match rs { + Ok(tag) => { + tail.tag.copy_from_slice(tag.as_slice()); + + let pm_header = zc_packet.mut_peer_manager_header().unwrap(); + pm_header.set_encrypted(true); + zc_packet.mut_inner().extend_from_slice(tail.as_bytes()); + Ok(()) + } + Err(_) => Err(Error::EncryptionFailed), + }; + } +} + +#[cfg(test)] +mod tests { + use crate::{ + peers::encrypt::{aes_gcm::AesGcmCipher, Encryptor}, + tunnel::packet_def::{ZCPacket, AES_GCM_ENCRYPTION_RESERVED}, + }; + + #[test] + fn test_aes_gcm_cipher() { + let key = [0u8; 16]; + let cipher = AesGcmCipher::new_128(key); + let text = b"1234567"; + let mut packet = ZCPacket::new_with_payload(text); + packet.fill_peer_manager_hdr(0, 0, 0); + cipher.encrypt(&mut packet).unwrap(); + assert_eq!( + packet.payload().len(), + text.len() + AES_GCM_ENCRYPTION_RESERVED + ); + assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), true); + + cipher.decrypt(&mut packet).unwrap(); + assert_eq!(packet.payload(), text); + assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), false); + } +} diff --git a/rust/easytier/src/peers/encrypt/mod.rs b/rust/easytier/src/peers/encrypt/mod.rs new file mode 100644 index 0000000..9bdb485 --- /dev/null +++ b/rust/easytier/src/peers/encrypt/mod.rs @@ -0,0 +1,41 @@ +use crate::tunnel::packet_def::ZCPacket; + +#[cfg(feature = "wireguard")] +pub mod ring_aes_gcm; + +#[cfg(feature = "aes-gcm")] +pub mod aes_gcm; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("packet is too short. len: {0}")] + PacketTooShort(usize), + #[error("decryption failed")] + DecryptionFailed, + #[error("encryption failed")] + EncryptionFailed, + #[error("invalid tag. tag: {0:?}")] + InvalidTag(Vec), +} + +pub trait Encryptor: Send + Sync + 'static { + fn encrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error>; + fn decrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error>; +} + +pub struct NullCipher; + +impl Encryptor for NullCipher { + fn encrypt(&self, _zc_packet: &mut ZCPacket) -> Result<(), Error> { + Ok(()) + } + + fn decrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> { + let pm_header = zc_packet.peer_manager_header().unwrap(); + if pm_header.is_encrypted() { + return Err(Error::DecryptionFailed); + } else { + Ok(()) + } + } +} diff --git a/rust/easytier/src/peers/encrypt/ring_aes_gcm.rs b/rust/easytier/src/peers/encrypt/ring_aes_gcm.rs new file mode 100644 index 0000000..603c25b --- /dev/null +++ b/rust/easytier/src/peers/encrypt/ring_aes_gcm.rs @@ -0,0 +1,161 @@ +use rand::RngCore; +use ring::aead::{self}; +use ring::aead::{LessSafeKey, UnboundKey}; +use zerocopy::{AsBytes, FromBytes}; + +use crate::tunnel::packet_def::{AesGcmTail, ZCPacket, AES_GCM_ENCRYPTION_RESERVED}; + +use super::{Encryptor, Error}; + +#[derive(Clone)] +pub struct AesGcmCipher { + pub(crate) cipher: AesGcmEnum, +} + +pub enum AesGcmEnum { + AesGCM128(LessSafeKey, [u8; 16]), + AesGCM256(LessSafeKey, [u8; 32]), +} + +impl Clone for AesGcmEnum { + fn clone(&self) -> Self { + match &self { + AesGcmEnum::AesGCM128(_, key) => { + let c = + LessSafeKey::new(UnboundKey::new(&aead::AES_128_GCM, key.as_slice()).unwrap()); + AesGcmEnum::AesGCM128(c, *key) + } + AesGcmEnum::AesGCM256(_, key) => { + let c = + LessSafeKey::new(UnboundKey::new(&aead::AES_256_GCM, key.as_slice()).unwrap()); + AesGcmEnum::AesGCM256(c, *key) + } + } + } +} + +impl AesGcmCipher { + pub fn new_128(key: [u8; 16]) -> Self { + let cipher = LessSafeKey::new(UnboundKey::new(&aead::AES_128_GCM, &key).unwrap()); + Self { + cipher: AesGcmEnum::AesGCM128(cipher, key), + } + } + + pub fn new_256(key: [u8; 32]) -> Self { + let cipher = LessSafeKey::new(UnboundKey::new(&aead::AES_256_GCM, &key).unwrap()); + Self { + cipher: AesGcmEnum::AesGCM256(cipher, key), + } + } +} + +impl Encryptor for AesGcmCipher { + fn decrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> { + let pm_header = zc_packet.peer_manager_header().unwrap(); + if !pm_header.is_encrypted() { + return Ok(()); + } + + let payload_len = zc_packet.payload().len(); + if payload_len < AES_GCM_ENCRYPTION_RESERVED { + return Err(Error::PacketTooShort(zc_packet.payload().len())); + } + + let text_and_tag_len = payload_len - AES_GCM_ENCRYPTION_RESERVED + 16; + + let aes_tail = AesGcmTail::ref_from_suffix(zc_packet.payload()).unwrap(); + let nonce = aead::Nonce::assume_unique_for_key(aes_tail.nonce.clone()); + + let rs = match &self.cipher { + AesGcmEnum::AesGCM128(cipher, _) => cipher.open_in_place( + nonce, + aead::Aad::empty(), + &mut zc_packet.mut_payload()[..text_and_tag_len], + ), + AesGcmEnum::AesGCM256(cipher, _) => cipher.open_in_place( + nonce, + aead::Aad::empty(), + &mut zc_packet.mut_payload()[..text_and_tag_len], + ), + }; + if let Err(_) = rs { + return Err(Error::DecryptionFailed); + } + + let pm_header = zc_packet.mut_peer_manager_header().unwrap(); + pm_header.set_encrypted(false); + let old_len = zc_packet.buf_len(); + zc_packet + .mut_inner() + .truncate(old_len - AES_GCM_ENCRYPTION_RESERVED); + return Ok(()); + } + + fn encrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> { + let pm_header = zc_packet.peer_manager_header().unwrap(); + if pm_header.is_encrypted() { + tracing::warn!(?zc_packet, "packet is already encrypted"); + return Ok(()); + } + + let mut tail = AesGcmTail::default(); + rand::thread_rng().fill_bytes(&mut tail.nonce); + let nonce = aead::Nonce::assume_unique_for_key(tail.nonce.clone()); + + let rs = match &self.cipher { + AesGcmEnum::AesGCM128(cipher, _) => cipher.seal_in_place_separate_tag( + nonce, + aead::Aad::empty(), + zc_packet.mut_payload(), + ), + AesGcmEnum::AesGCM256(cipher, _) => cipher.seal_in_place_separate_tag( + nonce, + aead::Aad::empty(), + zc_packet.mut_payload(), + ), + }; + return match rs { + Ok(tag) => { + let tag = tag.as_ref(); + if tag.len() != 16 { + return Err(Error::InvalidTag(tag.to_vec())); + } + tail.tag.copy_from_slice(tag); + + let pm_header = zc_packet.mut_peer_manager_header().unwrap(); + pm_header.set_encrypted(true); + zc_packet.mut_inner().extend_from_slice(tail.as_bytes()); + Ok(()) + } + Err(_) => Err(Error::EncryptionFailed), + }; + } +} + +#[cfg(test)] +mod tests { + use crate::{ + peers::encrypt::{ring_aes_gcm::AesGcmCipher, Encryptor}, + tunnel::packet_def::{ZCPacket, AES_GCM_ENCRYPTION_RESERVED}, + }; + + #[test] + fn test_aes_gcm_cipher() { + let key = [0u8; 16]; + let cipher = AesGcmCipher::new_128(key); + let text = b"1234567"; + let mut packet = ZCPacket::new_with_payload(text); + packet.fill_peer_manager_hdr(0, 0, 0); + cipher.encrypt(&mut packet).unwrap(); + assert_eq!( + packet.payload().len(), + text.len() + AES_GCM_ENCRYPTION_RESERVED + ); + assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), true); + + cipher.decrypt(&mut packet).unwrap(); + assert_eq!(packet.payload(), text); + assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), false); + } +} diff --git a/rust/easytier/src/peers/foreign_network_client.rs b/rust/easytier/src/peers/foreign_network_client.rs new file mode 100644 index 0000000..84ffab5 --- /dev/null +++ b/rust/easytier/src/peers/foreign_network_client.rs @@ -0,0 +1,102 @@ +use std::sync::{Arc, Mutex}; + +use crate::{ + common::{error::Error, global_ctx::ArcGlobalCtx, scoped_task::ScopedTask, PeerId}, + tunnel::packet_def::ZCPacket, +}; + +use super::{peer_conn::PeerConn, peer_map::PeerMap, peer_rpc::PeerRpcManager, PacketRecvChan}; + +pub struct ForeignNetworkClient { + global_ctx: ArcGlobalCtx, + peer_rpc: Arc, + my_peer_id: PeerId, + + peer_map: Arc, + task: Mutex>>, +} + +impl ForeignNetworkClient { + pub fn new( + global_ctx: ArcGlobalCtx, + packet_sender_to_mgr: PacketRecvChan, + peer_rpc: Arc, + my_peer_id: PeerId, + ) -> Self { + let peer_map = Arc::new(PeerMap::new( + packet_sender_to_mgr, + global_ctx.clone(), + my_peer_id, + )); + Self { + global_ctx, + peer_rpc, + my_peer_id, + + peer_map, + task: Mutex::new(None), + } + } + + pub async fn add_new_peer_conn(&self, peer_conn: PeerConn) { + tracing::warn!(peer_conn = ?peer_conn.get_conn_info(), network = ?peer_conn.get_network_identity(), "add new peer conn in foreign network client"); + self.peer_map.add_new_peer_conn(peer_conn).await + } + + pub fn has_next_hop(&self, peer_id: PeerId) -> bool { + self.get_next_hop(peer_id).is_some() + } + + pub async fn list_public_peers(&self) -> Vec { + self.peer_map.list_peers().await + } + + pub fn get_next_hop(&self, peer_id: PeerId) -> Option { + if self.peer_map.has_peer(peer_id) { + return Some(peer_id.clone()); + } + None + } + + pub async fn send_msg(&self, msg: ZCPacket, peer_id: PeerId) -> Result<(), Error> { + if let Some(next_hop) = self.get_next_hop(peer_id) { + let ret = self.peer_map.send_msg_directly(msg, next_hop).await; + if ret.is_err() { + tracing::error!( + ?ret, + ?peer_id, + ?next_hop, + "foreign network client send msg failed" + ); + } else { + tracing::info!( + ?peer_id, + ?next_hop, + "foreign network client send msg success" + ); + } + return ret; + } + Err(Error::RouteError(Some("no next hop".to_string()))) + } + + pub async fn run(&self) { + let peer_map = Arc::downgrade(&self.peer_map); + *self.task.lock().unwrap() = Some( + tokio::spawn(async move { + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + let Some(peer_map) = peer_map.upgrade() else { + break; + }; + peer_map.clean_peer_without_conn().await; + } + }) + .into(), + ); + } + + pub fn get_peer_map(&self) -> Arc { + self.peer_map.clone() + } +} diff --git a/rust/easytier/src/peers/foreign_network_manager.rs b/rust/easytier/src/peers/foreign_network_manager.rs new file mode 100644 index 0000000..dfc143c --- /dev/null +++ b/rust/easytier/src/peers/foreign_network_manager.rs @@ -0,0 +1,1085 @@ +/* +foreign_network_manager is used to forward packets of other networks. currently +only forward packets of peers that directly connected to this node. + +in future, with the help wo peer center we can forward packets of peers that +connected to any node in the local network. +*/ +use std::{ + sync::{Arc, Weak}, + time::SystemTime, +}; + +use dashmap::DashMap; +use tokio::{ + sync::{ + mpsc::{self, UnboundedReceiver, UnboundedSender}, + Mutex, + }, + task::JoinSet, +}; + +use crate::{ + common::{ + config::{ConfigLoader, TomlConfigLoader}, + error::Error, + global_ctx::{ArcGlobalCtx, GlobalCtx, GlobalCtxEvent, NetworkIdentity}, + join_joinset_background, + stun::MockStunInfoCollector, + PeerId, + }, + peers::route_trait::{Route, RouteInterface}, + proto::{ + cli::{ForeignNetworkEntryPb, ListForeignNetworkResponse, PeerInfo}, + common::NatType, + peer_rpc::DirectConnectorRpcServer, + }, + tunnel::packet_def::{PacketType, ZCPacket}, +}; + +use super::{ + create_packet_recv_chan, + peer_conn::PeerConn, + peer_map::PeerMap, + peer_ospf_route::PeerRoute, + peer_rpc::{PeerRpcManager, PeerRpcManagerTransport}, + peer_rpc_service::DirectConnectorManagerRpcServer, + recv_packet_from_chan, + route_trait::NextHopPolicy, + PacketRecvChan, PacketRecvChanReceiver, +}; + +#[async_trait::async_trait] +#[auto_impl::auto_impl(&, Box, Arc)] +pub trait GlobalForeignNetworkAccessor: Send + Sync + 'static { + async fn list_global_foreign_peer(&self, network_identity: &NetworkIdentity) -> Vec; +} + +struct ForeignNetworkEntry { + my_peer_id: PeerId, + + global_ctx: ArcGlobalCtx, + network: NetworkIdentity, + peer_map: Arc, + relay_data: bool, + pm_packet_sender: Mutex>, + + peer_rpc: Arc, + rpc_sender: UnboundedSender, + + packet_recv: Mutex>, + + tasks: Mutex>, +} + +impl ForeignNetworkEntry { + fn new( + network: NetworkIdentity, + global_ctx: ArcGlobalCtx, + my_peer_id: PeerId, + relay_data: bool, + pm_packet_sender: PacketRecvChan, + ) -> Self { + let foreign_global_ctx = Self::build_foreign_global_ctx(&network, global_ctx.clone()); + + let (packet_sender, packet_recv) = create_packet_recv_chan(); + + let peer_map = Arc::new(PeerMap::new( + packet_sender, + foreign_global_ctx.clone(), + my_peer_id, + )); + + let (peer_rpc, rpc_transport_sender) = Self::build_rpc_tspt(my_peer_id, peer_map.clone()); + + peer_rpc.rpc_server().registry().register( + DirectConnectorRpcServer::new(DirectConnectorManagerRpcServer::new( + foreign_global_ctx.clone(), + )), + &network.network_name, + ); + + Self { + my_peer_id, + + global_ctx: foreign_global_ctx, + network, + peer_map, + relay_data, + pm_packet_sender: Mutex::new(Some(pm_packet_sender)), + + peer_rpc, + rpc_sender: rpc_transport_sender, + + packet_recv: Mutex::new(Some(packet_recv)), + + tasks: Mutex::new(JoinSet::new()), + } + } + + fn build_foreign_global_ctx( + network: &NetworkIdentity, + global_ctx: ArcGlobalCtx, + ) -> ArcGlobalCtx { + let config = TomlConfigLoader::default(); + config.set_network_identity(network.clone()); + config.set_hostname(Some(format!("PublicServer_{}", global_ctx.get_hostname()))); + + let foreign_global_ctx = Arc::new(GlobalCtx::new(config)); + foreign_global_ctx.replace_stun_info_collector(Box::new(MockStunInfoCollector { + udp_nat_type: NatType::Unknown, + })); + + let mut feature_flag = global_ctx.get_feature_flags(); + feature_flag.is_public_server = true; + foreign_global_ctx.set_feature_flags(feature_flag); + + for u in global_ctx.get_running_listeners().into_iter() { + foreign_global_ctx.add_running_listener(u); + } + + foreign_global_ctx + } + + fn build_rpc_tspt( + my_peer_id: PeerId, + peer_map: Arc, + ) -> (Arc, UnboundedSender) { + struct RpcTransport { + my_peer_id: PeerId, + peer_map: Weak, + + packet_recv: Mutex>, + } + + #[async_trait::async_trait] + impl PeerRpcManagerTransport for RpcTransport { + fn my_peer_id(&self) -> PeerId { + self.my_peer_id + } + + async fn send(&self, msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error> { + tracing::debug!( + "foreign network manager send rpc to peer: {:?}", + dst_peer_id + ); + let peer_map = self + .peer_map + .upgrade() + .ok_or(anyhow::anyhow!("peer map is gone"))?; + + // send to ourselves so we can handle it in forward logic. + peer_map.send_msg_directly(msg, self.my_peer_id).await + } + + async fn recv(&self) -> Result { + if let Some(o) = self.packet_recv.lock().await.recv().await { + tracing::info!("recv rpc packet in foreign network manager rpc transport"); + Ok(o) + } else { + Err(Error::Unknown) + } + } + } + + impl Drop for RpcTransport { + fn drop(&mut self) { + tracing::debug!( + "drop rpc transport for foreign network manager, my_peer_id: {:?}", + self.my_peer_id + ); + } + } + + let (rpc_transport_sender, peer_rpc_tspt_recv) = mpsc::unbounded_channel(); + let tspt = RpcTransport { + my_peer_id, + peer_map: Arc::downgrade(&peer_map), + packet_recv: Mutex::new(peer_rpc_tspt_recv), + }; + + let peer_rpc = Arc::new(PeerRpcManager::new(tspt)); + (peer_rpc, rpc_transport_sender) + } + + async fn prepare_route( + &self, + my_peer_id: PeerId, + accessor: Box, + ) { + struct Interface { + my_peer_id: PeerId, + peer_map: Weak, + network_identity: NetworkIdentity, + accessor: Box, + } + + #[async_trait::async_trait] + impl RouteInterface for Interface { + async fn list_peers(&self) -> Vec { + let Some(peer_map) = self.peer_map.upgrade() else { + return vec![]; + }; + + let mut global = self + .accessor + .list_global_foreign_peer(&self.network_identity) + .await; + let local = peer_map.list_peers_with_conn().await; + global.extend(local.iter().cloned()); + global + .into_iter() + .filter(|x| *x != self.my_peer_id) + .collect() + } + + fn my_peer_id(&self) -> PeerId { + self.my_peer_id + } + } + + let route = PeerRoute::new(my_peer_id, self.global_ctx.clone(), self.peer_rpc.clone()); + route + .open(Box::new(Interface { + my_peer_id, + network_identity: self.network.clone(), + peer_map: Arc::downgrade(&self.peer_map), + accessor, + })) + .await + .unwrap(); + + self.peer_map.add_route(Arc::new(Box::new(route))).await; + } + + async fn start_packet_recv(&self) { + let mut recv = self.packet_recv.lock().await.take().unwrap(); + let my_node_id = self.my_peer_id; + let rpc_sender = self.rpc_sender.clone(); + let peer_map = self.peer_map.clone(); + let relay_data = self.relay_data; + let pm_sender = self.pm_packet_sender.lock().await.take().unwrap(); + let network_name = self.network.network_name.clone(); + + self.tasks.lock().await.spawn(async move { + while let Ok(zc_packet) = recv_packet_from_chan(&mut recv).await { + let Some(hdr) = zc_packet.peer_manager_header() else { + tracing::warn!("invalid packet, skip"); + continue; + }; + tracing::info!(?hdr, "recv packet in foreign network manager"); + let to_peer_id = hdr.to_peer_id.get(); + if to_peer_id == my_node_id { + if hdr.packet_type == PacketType::TaRpc as u8 + || hdr.packet_type == PacketType::RpcReq as u8 + || hdr.packet_type == PacketType::RpcResp as u8 + { + rpc_sender.send(zc_packet).unwrap(); + continue; + } + tracing::trace!(?hdr, "ignore packet in foreign network"); + } else { + if !relay_data && hdr.packet_type == PacketType::Data as u8 { + continue; + } + + let gateway_peer_id = peer_map + .get_gateway_peer_id(to_peer_id, NextHopPolicy::LeastHop) + .await; + + if gateway_peer_id.is_some() && peer_map.has_peer(gateway_peer_id.unwrap()) { + if let Err(e) = peer_map + .send_msg_directly(zc_packet, gateway_peer_id.unwrap()) + .await + { + tracing::error!( + ?e, + "send packet to foreign peer inside peer map failed" + ); + } + } else { + let mut foreign_packet = ZCPacket::new_for_foreign_network( + &network_name, + to_peer_id, + &zc_packet, + ); + foreign_packet.fill_peer_manager_hdr( + my_node_id, + gateway_peer_id.unwrap_or(to_peer_id), + PacketType::ForeignNetworkPacket as u8, + ); + if let Err(e) = pm_sender.send(foreign_packet).await { + tracing::error!("send packet to peer with pm failed: {:?}", e); + } + } + } + } + }); + } + + async fn prepare(&self, my_peer_id: PeerId, accessor: Box) { + self.prepare_route(my_peer_id, accessor).await; + self.start_packet_recv().await; + self.peer_rpc.run(); + } +} + +impl Drop for ForeignNetworkEntry { + fn drop(&mut self) { + self.peer_rpc + .rpc_server() + .registry() + .unregister_by_domain(&self.network.network_name); + + tracing::debug!(self.my_peer_id, ?self.network, "drop foreign network entry"); + } +} + +struct ForeignNetworkManagerData { + network_peer_maps: DashMap>, + peer_network_map: DashMap, + network_peer_last_update: DashMap, + accessor: Arc>, + lock: std::sync::Mutex<()>, +} + +impl ForeignNetworkManagerData { + fn get_peer_network(&self, peer_id: PeerId) -> Option { + self.peer_network_map.get(&peer_id).map(|v| v.clone()) + } + + fn get_network_entry(&self, network_name: &str) -> Option> { + self.network_peer_maps.get(network_name).map(|v| v.clone()) + } + + fn remove_peer(&self, peer_id: PeerId, network_name: &String) { + let _l = self.lock.lock().unwrap(); + self.peer_network_map.remove(&peer_id); + if let Some(_) = self + .network_peer_maps + .remove_if(network_name, |_, v| v.peer_map.is_empty()) + { + self.network_peer_last_update.remove(network_name); + } + } + + async fn clear_no_conn_peer(&self, network_name: &String) { + let Some(peer_map) = self + .network_peer_maps + .get(network_name) + .and_then(|v| Some(v.peer_map.clone())) + else { + return; + }; + peer_map.clean_peer_without_conn().await; + } + + fn remove_network(&self, network_name: &String) { + let _l = self.lock.lock().unwrap(); + self.peer_network_map.retain(|_, v| v != network_name); + self.network_peer_maps.remove(network_name); + self.network_peer_last_update.remove(network_name); + } + + async fn get_or_insert_entry( + &self, + network_identity: &NetworkIdentity, + my_peer_id: PeerId, + dst_peer_id: PeerId, + relay_data: bool, + global_ctx: &ArcGlobalCtx, + pm_packet_sender: &PacketRecvChan, + ) -> (Arc, bool) { + let mut new_added = false; + + let l = self.lock.lock().unwrap(); + let entry = self + .network_peer_maps + .entry(network_identity.network_name.clone()) + .or_insert_with(|| { + new_added = true; + Arc::new(ForeignNetworkEntry::new( + network_identity.clone(), + global_ctx.clone(), + my_peer_id, + relay_data, + pm_packet_sender.clone(), + )) + }) + .clone(); + + self.peer_network_map + .insert(dst_peer_id, network_identity.network_name.clone()); + + self.network_peer_last_update + .insert(network_identity.network_name.clone(), SystemTime::now()); + + drop(l); + + if new_added { + entry + .prepare(my_peer_id, Box::new(self.accessor.clone())) + .await; + } + + (entry, new_added) + } +} + +pub const FOREIGN_NETWORK_SERVICE_ID: u32 = 1; + +pub struct ForeignNetworkManager { + my_peer_id: PeerId, + global_ctx: ArcGlobalCtx, + packet_sender_to_mgr: PacketRecvChan, + + data: Arc, + + tasks: Arc>>, +} + +impl ForeignNetworkManager { + pub fn new( + my_peer_id: PeerId, + global_ctx: ArcGlobalCtx, + packet_sender_to_mgr: PacketRecvChan, + accessor: Box, + ) -> Self { + let data = Arc::new(ForeignNetworkManagerData { + network_peer_maps: DashMap::new(), + peer_network_map: DashMap::new(), + network_peer_last_update: DashMap::new(), + accessor: Arc::new(accessor), + lock: std::sync::Mutex::new(()), + }); + + let tasks = Arc::new(std::sync::Mutex::new(JoinSet::new())); + join_joinset_background(tasks.clone(), "ForeignNetworkManager".to_string()); + + Self { + my_peer_id, + global_ctx, + packet_sender_to_mgr, + + data, + + tasks, + } + } + + pub async fn add_peer_conn(&self, peer_conn: PeerConn) -> Result<(), Error> { + tracing::info!(peer_conn = ?peer_conn.get_conn_info(), network = ?peer_conn.get_network_identity(), "add new peer conn in foreign network manager"); + + let relay_peer_rpc = self.global_ctx.get_flags().relay_all_peer_rpc; + let ret = self + .global_ctx + .check_network_in_whitelist(&peer_conn.get_network_identity().network_name) + .map_err(Into::into); + if ret.is_err() && !relay_peer_rpc { + return ret; + } + + let (entry, new_added) = self + .data + .get_or_insert_entry( + &peer_conn.get_network_identity(), + self.my_peer_id, + peer_conn.get_peer_id(), + !ret.is_err(), + &self.global_ctx, + &self.packet_sender_to_mgr, + ) + .await; + + if entry.network != peer_conn.get_network_identity() { + if new_added { + self.data + .remove_network(&entry.network.network_name.clone()); + } + return Err(anyhow::anyhow!( + "network secret not match. exp: {:?} real: {:?}", + entry.network, + peer_conn.get_network_identity() + ) + .into()); + } + + if new_added { + self.start_event_handler(&entry).await; + } + + Ok(entry.peer_map.add_new_peer_conn(peer_conn).await) + } + + async fn start_event_handler(&self, entry: &ForeignNetworkEntry) { + let data = self.data.clone(); + let network_name = entry.network.network_name.clone(); + let mut s = entry.global_ctx.subscribe(); + self.tasks.lock().unwrap().spawn(async move { + while let Ok(e) = s.recv().await { + match &e { + GlobalCtxEvent::PeerRemoved(peer_id) => { + tracing::info!(?e, "remove peer from foreign network manager"); + data.remove_peer(*peer_id, &network_name); + data.network_peer_last_update + .insert(network_name.clone(), SystemTime::now()); + } + GlobalCtxEvent::PeerConnRemoved(..) => { + tracing::info!(?e, "clear no conn peer from foreign network manager"); + data.clear_no_conn_peer(&network_name).await; + } + GlobalCtxEvent::PeerAdded(_) => { + tracing::info!(?e, "add peer to foreign network manager"); + data.network_peer_last_update + .insert(network_name.clone(), SystemTime::now()); + } + _ => continue, + } + } + // if lagged or recv done just remove the network + tracing::error!("global event handler at foreign network manager exit"); + data.remove_network(&network_name); + }); + } + + pub async fn list_foreign_networks(&self) -> ListForeignNetworkResponse { + let mut ret = ListForeignNetworkResponse::default(); + let networks = self + .data + .network_peer_maps + .iter() + .map(|v| v.key().clone()) + .collect::>(); + + for network_name in networks { + let Some(item) = self + .data + .network_peer_maps + .get(&network_name) + .map(|v| v.clone()) + else { + continue; + }; + + let mut entry = ForeignNetworkEntryPb { + network_secret_digest: item + .network + .network_secret_digest + .unwrap_or_default() + .to_vec(), + ..Default::default() + }; + for peer in item.peer_map.list_peers().await { + let mut peer_info = PeerInfo::default(); + peer_info.peer_id = peer; + peer_info.conns = item.peer_map.list_peer_conns(peer).await.unwrap_or(vec![]); + entry.peers.push(peer_info); + } + + ret.foreign_networks.insert(network_name, entry); + } + ret + } + + pub fn get_foreign_network_last_update(&self, network_name: &str) -> Option { + self.data + .network_peer_last_update + .get(network_name) + .map(|v| v.clone()) + } + + pub async fn send_msg_to_peer( + &self, + network_name: &str, + dst_peer_id: PeerId, + msg: ZCPacket, + ) -> Result<(), Error> { + if let Some(entry) = self.data.get_network_entry(network_name) { + entry + .peer_map + .send_msg(msg, dst_peer_id, NextHopPolicy::LeastHop) + .await + } else { + Err(Error::RouteError(Some("network not found".to_string()))) + } + } +} + +impl Drop for ForeignNetworkManager { + fn drop(&mut self) { + self.data.peer_network_map.clear(); + self.data.network_peer_maps.clear(); + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use crate::{ + common::global_ctx::tests::get_mock_global_ctx_with_network, + connector::udp_hole_punch::tests::{ + create_mock_peer_manager_with_mock_stun, replace_stun_info_collector, + }, + peers::{ + peer_manager::{PeerManager, RouteAlgoType}, + tests::{connect_peer_manager, wait_route_appear}, + }, + proto::common::NatType, + set_global_var, + tunnel::common::tests::wait_for_condition, + }; + + use super::*; + + async fn create_mock_peer_manager_for_foreign_network_ext( + network: &str, + secret: &str, + ) -> Arc { + let (s, _r) = create_packet_recv_chan(); + let peer_mgr = Arc::new(PeerManager::new( + RouteAlgoType::Ospf, + get_mock_global_ctx_with_network(Some(NetworkIdentity::new( + network.to_string(), + secret.to_string(), + ))), + s, + )); + replace_stun_info_collector(peer_mgr.clone(), NatType::Unknown); + peer_mgr.run().await.unwrap(); + peer_mgr + } + + async fn create_mock_peer_manager_for_foreign_network(network: &str) -> Arc { + create_mock_peer_manager_for_foreign_network_ext(network, network).await + } + + #[tokio::test] + async fn foreign_network_basic() { + let pm_center = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + tracing::debug!("pm_center: {:?}", pm_center.my_peer_id()); + + let pma_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + let pmb_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + tracing::debug!( + "pma_net1: {:?}, pmb_net1: {:?}", + pma_net1.my_peer_id(), + pmb_net1.my_peer_id() + ); + connect_peer_manager(pma_net1.clone(), pm_center.clone()).await; + connect_peer_manager(pmb_net1.clone(), pm_center.clone()).await; + wait_route_appear(pma_net1.clone(), pmb_net1.clone()) + .await + .unwrap(); + assert_eq!(2, pma_net1.list_routes().await.len()); + assert_eq!(2, pmb_net1.list_routes().await.len()); + + println!("{:?}", pmb_net1.list_routes().await); + + let rpc_resp = pm_center + .get_foreign_network_manager() + .list_foreign_networks() + .await; + assert_eq!(1, rpc_resp.foreign_networks.len()); + assert_eq!(2, rpc_resp.foreign_networks["net1"].peers.len()); + } + + async fn foreign_network_whitelist_helper(name: String) { + let pm_center = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + tracing::debug!("pm_center: {:?}", pm_center.my_peer_id()); + let mut flag = pm_center.get_global_ctx().get_flags(); + flag.relay_network_whitelist = vec!["net1".to_string(), "net2*".to_string()].join(" "); + pm_center.get_global_ctx().config.set_flags(flag); + + let pma_net1 = create_mock_peer_manager_for_foreign_network(name.as_str()).await; + + let (a_ring, b_ring) = crate::tunnel::ring::create_ring_tunnel_pair(); + let b_mgr_copy = pm_center.clone(); + let s_ret = tokio::spawn(async move { b_mgr_copy.add_tunnel_as_server(b_ring).await }); + + pma_net1.add_client_tunnel(a_ring).await.unwrap(); + + s_ret.await.unwrap().unwrap(); + } + + #[tokio::test] + async fn foreign_network_whitelist() { + foreign_network_whitelist_helper("net1".to_string()).await; + foreign_network_whitelist_helper("net2".to_string()).await; + foreign_network_whitelist_helper("net2abc".to_string()).await; + } + + #[tokio::test] + async fn only_relay_peer_rpc() { + let pm_center = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let mut flag = pm_center.get_global_ctx().get_flags(); + flag.relay_network_whitelist = "".to_string(); + flag.relay_all_peer_rpc = true; + pm_center.get_global_ctx().config.set_flags(flag); + tracing::debug!("pm_center: {:?}", pm_center.my_peer_id()); + + let pma_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + let pmb_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + tracing::debug!( + "pma_net1: {:?}, pmb_net1: {:?}", + pma_net1.my_peer_id(), + pmb_net1.my_peer_id() + ); + connect_peer_manager(pma_net1.clone(), pm_center.clone()).await; + connect_peer_manager(pmb_net1.clone(), pm_center.clone()).await; + wait_route_appear(pma_net1.clone(), pmb_net1.clone()) + .await + .unwrap(); + assert_eq!(2, pma_net1.list_routes().await.len()); + assert_eq!(2, pmb_net1.list_routes().await.len()); + } + + #[tokio::test] + #[should_panic] + async fn foreign_network_whitelist_fail() { + foreign_network_whitelist_helper("net3".to_string()).await; + } + + #[tokio::test] + async fn test_foreign_network_manager() { + let pm_center = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let pm_center2 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + connect_peer_manager(pm_center.clone(), pm_center2.clone()).await; + + tracing::debug!( + "pm_center: {:?}, pm_center2: {:?}", + pm_center.my_peer_id(), + pm_center2.my_peer_id() + ); + + let pma_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + let pmb_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + connect_peer_manager(pma_net1.clone(), pm_center.clone()).await; + connect_peer_manager(pmb_net1.clone(), pm_center.clone()).await; + + tracing::debug!( + "pma_net1: {:?}, pmb_net1: {:?}", + pma_net1.my_peer_id(), + pmb_net1.my_peer_id() + ); + + wait_route_appear(pma_net1.clone(), pmb_net1.clone()) + .await + .unwrap(); + + assert_eq!( + vec![pm_center.my_peer_id()], + pma_net1 + .get_foreign_network_client() + .get_peer_map() + .list_peers() + .await + ); + assert_eq!( + vec![pm_center.my_peer_id()], + pmb_net1 + .get_foreign_network_client() + .get_peer_map() + .list_peers() + .await + ); + + assert_eq!(2, pma_net1.list_routes().await.len()); + assert_eq!(2, pmb_net1.list_routes().await.len()); + + let pmc_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + connect_peer_manager(pmc_net1.clone(), pm_center.clone()).await; + wait_route_appear(pma_net1.clone(), pmc_net1.clone()) + .await + .unwrap(); + wait_route_appear(pmb_net1.clone(), pmc_net1.clone()) + .await + .unwrap(); + assert_eq!(3, pmc_net1.list_routes().await.len()); + + tracing::debug!("pmc_net1: {:?}", pmc_net1.my_peer_id()); + + let pma_net2 = create_mock_peer_manager_for_foreign_network("net2").await; + let pmb_net2 = create_mock_peer_manager_for_foreign_network("net2").await; + tracing::debug!( + "pma_net2: {:?}, pmb_net2: {:?}", + pma_net2.my_peer_id(), + pmb_net2.my_peer_id() + ); + connect_peer_manager(pma_net2.clone(), pm_center.clone()).await; + connect_peer_manager(pmb_net2.clone(), pm_center.clone()).await; + wait_route_appear(pma_net2.clone(), pmb_net2.clone()) + .await + .unwrap(); + assert_eq!(2, pma_net2.list_routes().await.len()); + assert_eq!(2, pmb_net2.list_routes().await.len()); + + assert_eq!( + 5, + pm_center + .get_foreign_network_manager() + .data + .peer_network_map + .len() + ); + + assert_eq!( + 2, + pm_center + .get_foreign_network_manager() + .data + .network_peer_maps + .len() + ); + + let rpc_resp = pm_center + .get_foreign_network_manager() + .list_foreign_networks() + .await; + assert_eq!(2, rpc_resp.foreign_networks.len()); + assert_eq!(3, rpc_resp.foreign_networks["net1"].peers.len()); + assert_eq!(2, rpc_resp.foreign_networks["net2"].peers.len()); + + drop(pmb_net2); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + assert_eq!( + 4, + pm_center + .get_foreign_network_manager() + .data + .peer_network_map + .len() + ); + drop(pma_net2); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + assert_eq!( + 3, + pm_center + .get_foreign_network_manager() + .data + .peer_network_map + .len() + ); + assert_eq!( + 1, + pm_center + .get_foreign_network_manager() + .data + .network_peer_maps + .len() + ); + } + + #[tokio::test] + async fn test_disconnect_foreign_network() { + let pm_center = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + tracing::debug!("pm_center: {:?}", pm_center.my_peer_id()); + let pma_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + tracing::debug!("pma_net1: {:?}", pma_net1.my_peer_id(),); + + connect_peer_manager(pma_net1.clone(), pm_center.clone()).await; + + wait_for_condition( + || async { pma_net1.list_routes().await.len() == 1 }, + Duration::from_secs(5), + ) + .await; + + drop(pm_center); + wait_for_condition( + || async { pma_net1.list_routes().await.len() == 0 }, + Duration::from_secs(5), + ) + .await; + } + + #[tokio::test] + async fn test_foreign_network_manager_cluster() { + set_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, 1); + + let pm_center1 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let pm_center2 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let pm_center3 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + + connect_peer_manager(pm_center1.clone(), pm_center2.clone()).await; + connect_peer_manager(pm_center2.clone(), pm_center3.clone()).await; + + tracing::debug!( + "pm_center: {:?}, pm_center2: {:?}", + pm_center1.my_peer_id(), + pm_center2.my_peer_id() + ); + + let pma_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + let pmb_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + connect_peer_manager(pma_net1.clone(), pm_center1.clone()).await; + connect_peer_manager(pmb_net1.clone(), pm_center2.clone()).await; + + tracing::debug!( + "pma_net1: {:?}, pmb_net1: {:?}", + pma_net1.my_peer_id(), + pmb_net1.my_peer_id() + ); + + wait_route_appear(pma_net1.clone(), pmb_net1.clone()) + .await + .unwrap(); + + assert_eq!(3, pma_net1.list_routes().await.len(),); + + let pmc_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + connect_peer_manager(pmc_net1.clone(), pm_center3.clone()).await; + wait_route_appear(pma_net1.clone(), pmc_net1.clone()) + .await + .unwrap(); + assert_eq!(5, pma_net1.list_routes().await.len(),); + + println!( + "pm_center1: {:?}, pm_center2: {:?}, pm_center3: {:?}", + pm_center1.my_peer_id(), + pm_center2.my_peer_id(), + pm_center3.my_peer_id() + ); + println!( + "pma_net1: {:?}, pmb_net1: {:?}, pmc_net1: {:?}", + pma_net1.my_peer_id(), + pmb_net1.my_peer_id(), + pmc_net1.my_peer_id() + ); + + println!("drop pmc_net1, id: {:?}", pmc_net1.my_peer_id()); + + // foreign network node disconnect + drop(pmc_net1); + wait_for_condition( + || async { pma_net1.list_routes().await.len() == 3 }, + Duration::from_secs(15), + ) + .await; + + println!("drop pm_center1, id: {:?}", pm_center1.my_peer_id()); + drop(pm_center1); + wait_for_condition( + || async { pma_net1.list_routes().await.len() == 0 }, + Duration::from_secs(5), + ) + .await; + wait_for_condition( + || async { + let n = pmb_net1 + .get_route() + .get_next_hop(pma_net1.my_peer_id()) + .await; + n.is_none() + }, + Duration::from_secs(5), + ) + .await; + wait_for_condition( + || async { + // only remain pmb center + pmb_net1.list_routes().await.len() == 1 + }, + Duration::from_secs(15), + ) + .await; + } + + #[tokio::test] + async fn test_foreign_network_manager_cluster_multi_net() { + set_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, 1); + + let pm_center1 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let pm_center2 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let pm_center3 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + + connect_peer_manager(pm_center1.clone(), pm_center2.clone()).await; + connect_peer_manager(pm_center2.clone(), pm_center3.clone()).await; + + let pma_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + let pmb_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + connect_peer_manager(pma_net1.clone(), pm_center1.clone()).await; + connect_peer_manager(pmb_net1.clone(), pm_center2.clone()).await; + + let pma_net2 = create_mock_peer_manager_for_foreign_network("net2").await; + let pmb_net2 = create_mock_peer_manager_for_foreign_network("net2").await; + connect_peer_manager(pma_net2.clone(), pm_center2.clone()).await; + connect_peer_manager(pmb_net2.clone(), pm_center3.clone()).await; + + let pma_net3 = create_mock_peer_manager_for_foreign_network("net3").await; + let pmb_net3 = create_mock_peer_manager_for_foreign_network("net3").await; + connect_peer_manager(pma_net3.clone(), pm_center1.clone()).await; + connect_peer_manager(pmb_net3.clone(), pm_center3.clone()).await; + + let pma_net4 = create_mock_peer_manager_for_foreign_network("net4").await; + let pmb_net4 = create_mock_peer_manager_for_foreign_network("net4").await; + let pmc_net4 = create_mock_peer_manager_for_foreign_network("net4").await; + connect_peer_manager(pma_net4.clone(), pm_center1.clone()).await; + connect_peer_manager(pmb_net4.clone(), pm_center2.clone()).await; + connect_peer_manager(pmc_net4.clone(), pm_center3.clone()).await; + + tokio::time::sleep(Duration::from_secs(5)).await; + + wait_route_appear(pma_net1.clone(), pmb_net1.clone()) + .await + .unwrap(); + wait_route_appear(pma_net2.clone(), pmb_net2.clone()) + .await + .unwrap(); + wait_route_appear(pma_net3.clone(), pmb_net3.clone()) + .await + .unwrap(); + wait_route_appear(pma_net4.clone(), pmb_net4.clone()) + .await + .unwrap(); + wait_route_appear(pma_net4.clone(), pmc_net4.clone()) + .await + .unwrap(); + wait_route_appear(pmb_net4.clone(), pmc_net4.clone()) + .await + .unwrap(); + + assert_eq!(3, pma_net1.list_routes().await.len()); + assert_eq!(3, pmb_net1.list_routes().await.len()); + + assert_eq!(3, pma_net2.list_routes().await.len()); + assert_eq!(3, pmb_net2.list_routes().await.len()); + + assert_eq!(3, pma_net3.list_routes().await.len()); + assert_eq!(3, pmb_net3.list_routes().await.len()); + + assert_eq!(5, pma_net4.list_routes().await.len()); + assert_eq!(5, pmb_net4.list_routes().await.len()); + assert_eq!(5, pmc_net4.list_routes().await.len()); + + drop(pm_center3); + tokio::time::sleep(Duration::from_secs(5)).await; + assert_eq!(1, pma_net2.list_routes().await.len()); + assert_eq!(1, pma_net3.list_routes().await.len()); + assert_eq!(3, pma_net4.list_routes().await.len()); + } + + #[tokio::test] + async fn test_foreign_network_manager_cluster_secret_mismatch() { + set_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, 1); + + let pm_center1 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let pm_center2 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let pm_center3 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + + connect_peer_manager(pm_center1.clone(), pm_center2.clone()).await; + connect_peer_manager(pm_center2.clone(), pm_center3.clone()).await; + + let pma_net4 = create_mock_peer_manager_for_foreign_network_ext("net4", "1").await; + let pmb_net4 = create_mock_peer_manager_for_foreign_network_ext("net4", "2").await; + let pmc_net4 = create_mock_peer_manager_for_foreign_network_ext("net4", "3").await; + connect_peer_manager(pma_net4.clone(), pm_center1.clone()).await; + connect_peer_manager(pmb_net4.clone(), pm_center2.clone()).await; + connect_peer_manager(pmc_net4.clone(), pm_center3.clone()).await; + + tokio::time::sleep(Duration::from_secs(5)).await; + assert_eq!(1, pma_net4.list_routes().await.len()); + assert_eq!(1, pmb_net4.list_routes().await.len()); + assert_eq!(1, pmc_net4.list_routes().await.len()); + } +} diff --git a/rust/easytier/src/peers/mod.rs b/rust/easytier/src/peers/mod.rs new file mode 100644 index 0000000..bb9fd19 --- /dev/null +++ b/rust/easytier/src/peers/mod.rs @@ -0,0 +1,59 @@ +pub mod peer; +// pub mod peer_conn; +pub mod peer_conn; +pub mod peer_conn_ping; +pub mod peer_manager; +pub mod peer_map; +pub mod peer_ospf_route; +pub mod peer_rpc; +pub mod peer_rpc_service; +pub mod route_trait; +pub mod rpc_service; + +pub mod foreign_network_client; +pub mod foreign_network_manager; + +pub mod encrypt; + +pub mod peer_task; + +#[cfg(test)] +pub mod tests; + +use crate::tunnel::packet_def::ZCPacket; + +#[async_trait::async_trait] +#[auto_impl::auto_impl(Arc)] +pub trait PeerPacketFilter { + async fn try_process_packet_from_peer(&self, _zc_packet: ZCPacket) -> Option { + Some(_zc_packet) + } +} + +#[async_trait::async_trait] +#[auto_impl::auto_impl(Arc)] +pub trait NicPacketFilter { + async fn try_process_packet_from_nic(&self, data: &mut ZCPacket) -> bool; +} + +type BoxPeerPacketFilter = Box; +type BoxNicPacketFilter = Box; + +// pub type PacketRecvChan = tachyonix::Sender; +// pub type PacketRecvChanReceiver = tachyonix::Receiver; +// pub fn create_packet_recv_chan() -> (PacketRecvChan, PacketRecvChanReceiver) { +// tachyonix::channel(128) +// } +pub type PacketRecvChan = tokio::sync::mpsc::Sender; +pub type PacketRecvChanReceiver = tokio::sync::mpsc::Receiver; +pub fn create_packet_recv_chan() -> (PacketRecvChan, PacketRecvChanReceiver) { + tokio::sync::mpsc::channel(128) +} +pub async fn recv_packet_from_chan( + packet_recv_chan_receiver: &mut PacketRecvChanReceiver, +) -> Result { + packet_recv_chan_receiver + .recv() + .await + .ok_or(anyhow::anyhow!("recv_packet_from_chan failed")) +} diff --git a/rust/easytier/src/peers/peer.rs b/rust/easytier/src/peers/peer.rs new file mode 100644 index 0000000..248bfc2 --- /dev/null +++ b/rust/easytier/src/peers/peer.rs @@ -0,0 +1,231 @@ +use std::sync::Arc; + +use crossbeam::atomic::AtomicCell; +use dashmap::DashMap; + +use tokio::{select, sync::mpsc, task::JoinHandle}; + +use tracing::Instrument; + +use super::{ + peer_conn::{PeerConn, PeerConnId}, + PacketRecvChan, +}; +use crate::proto::cli::PeerConnInfo; +use crate::{ + common::{ + error::Error, + global_ctx::{ArcGlobalCtx, GlobalCtxEvent}, + PeerId, + }, + tunnel::packet_def::ZCPacket, +}; + +type ArcPeerConn = Arc; +type ConnMap = Arc>; + +pub struct Peer { + pub peer_node_id: PeerId, + conns: ConnMap, + global_ctx: ArcGlobalCtx, + + packet_recv_chan: PacketRecvChan, + + close_event_sender: mpsc::Sender, + close_event_listener: JoinHandle<()>, + + shutdown_notifier: Arc, + + default_conn_id: AtomicCell, +} + +impl Peer { + pub fn new( + peer_node_id: PeerId, + packet_recv_chan: PacketRecvChan, + global_ctx: ArcGlobalCtx, + ) -> Self { + let conns: ConnMap = Arc::new(DashMap::new()); + let (close_event_sender, mut close_event_receiver) = mpsc::channel(10); + let shutdown_notifier = Arc::new(tokio::sync::Notify::new()); + + let conns_copy = conns.clone(); + let shutdown_notifier_copy = shutdown_notifier.clone(); + let global_ctx_copy = global_ctx.clone(); + let close_event_listener = tokio::spawn( + async move { + loop { + select! { + ret = close_event_receiver.recv() => { + if ret.is_none() { + break; + } + let ret = ret.unwrap(); + tracing::warn!( + ?peer_node_id, + ?ret, + "notified that peer conn is closed", + ); + + if let Some((_, conn)) = conns_copy.remove(&ret) { + global_ctx_copy.issue_event(GlobalCtxEvent::PeerConnRemoved( + conn.get_conn_info(), + )); + } + } + + _ = shutdown_notifier_copy.notified() => { + close_event_receiver.close(); + tracing::warn!(?peer_node_id, "peer close event listener notified"); + } + } + } + tracing::info!("peer {} close event listener exit", peer_node_id); + } + .instrument(tracing::info_span!( + "peer_close_event_listener", + ?peer_node_id, + )), + ); + + Peer { + peer_node_id, + conns: conns.clone(), + packet_recv_chan, + global_ctx, + + close_event_sender, + close_event_listener, + + shutdown_notifier, + default_conn_id: AtomicCell::new(PeerConnId::default()), + } + } + + pub async fn add_peer_conn(&self, mut conn: PeerConn) { + conn.set_close_event_sender(self.close_event_sender.clone()); + conn.start_recv_loop(self.packet_recv_chan.clone()).await; + conn.start_pingpong(); + self.global_ctx + .issue_event(GlobalCtxEvent::PeerConnAdded(conn.get_conn_info())); + self.conns.insert(conn.get_conn_id(), Arc::new(conn)); + } + + async fn select_conn(&self) -> Option { + let default_conn_id = self.default_conn_id.load(); + if let Some(conn) = self.conns.get(&default_conn_id) { + return Some(conn.clone()); + } + + let conn = self.conns.iter().next(); + if conn.is_none() { + return None; + } + + let conn = conn.unwrap().clone(); + self.default_conn_id.store(conn.get_conn_id()); + Some(conn) + } + + pub async fn send_msg(&self, msg: ZCPacket) -> Result<(), Error> { + let Some(conn) = self.select_conn().await else { + return Err(Error::PeerNoConnectionError(self.peer_node_id)); + }; + conn.send_msg(msg).await?; + + Ok(()) + } + + pub async fn close_peer_conn(&self, conn_id: &PeerConnId) -> Result<(), Error> { + let has_key = self.conns.contains_key(conn_id); + if !has_key { + return Err(Error::NotFound); + } + self.close_event_sender.send(conn_id.clone()).await.unwrap(); + Ok(()) + } + + pub async fn list_peer_conns(&self) -> Vec { + let mut conns = vec![]; + for conn in self.conns.iter() { + // do not lock here, otherwise it will cause dashmap deadlock + conns.push(conn.clone()); + } + + let mut ret = Vec::new(); + for conn in conns { + ret.push(conn.get_conn_info()); + } + ret + } +} + +// pritn on drop +impl Drop for Peer { + fn drop(&mut self) { + self.shutdown_notifier.notify_one(); + tracing::info!("peer {} drop", self.peer_node_id); + } +} + +#[cfg(test)] +mod tests { + + use tokio::time::timeout; + + use crate::{ + common::{global_ctx::tests::get_mock_global_ctx, new_peer_id}, + peers::{create_packet_recv_chan, peer_conn::PeerConn}, + tunnel::ring::create_ring_tunnel_pair, + }; + + use super::Peer; + + #[tokio::test] + async fn close_peer() { + let (local_packet_send, _local_packet_recv) = create_packet_recv_chan(); + let (remote_packet_send, _remote_packet_recv) = create_packet_recv_chan(); + let global_ctx = get_mock_global_ctx(); + let local_peer = Peer::new(new_peer_id(), local_packet_send, global_ctx.clone()); + let remote_peer = Peer::new(new_peer_id(), remote_packet_send, global_ctx.clone()); + + let (local_tunnel, remote_tunnel) = create_ring_tunnel_pair(); + let mut local_peer_conn = + PeerConn::new(local_peer.peer_node_id, global_ctx.clone(), local_tunnel); + let mut remote_peer_conn = + PeerConn::new(remote_peer.peer_node_id, global_ctx.clone(), remote_tunnel); + + assert!(!local_peer_conn.handshake_done()); + assert!(!remote_peer_conn.handshake_done()); + + let (a, b) = tokio::join!( + local_peer_conn.do_handshake_as_client(), + remote_peer_conn.do_handshake_as_server() + ); + a.unwrap(); + b.unwrap(); + + let local_conn_id = local_peer_conn.get_conn_id(); + + local_peer.add_peer_conn(local_peer_conn).await; + remote_peer.add_peer_conn(remote_peer_conn).await; + + assert_eq!(local_peer.list_peer_conns().await.len(), 1); + assert_eq!(remote_peer.list_peer_conns().await.len(), 1); + + let close_handler = + tokio::spawn(async move { local_peer.close_peer_conn(&local_conn_id).await }); + + // wait for remote peer conn close + timeout(std::time::Duration::from_secs(5), async { + while (&remote_peer).list_peer_conns().await.len() != 0 { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + }) + .await + .unwrap(); + + println!("wait for close handler"); + close_handler.await.unwrap().unwrap(); + } +} diff --git a/rust/easytier/src/peers/peer_conn.rs b/rust/easytier/src/peers/peer_conn.rs new file mode 100644 index 0000000..9c5a78c --- /dev/null +++ b/rust/easytier/src/peers/peer_conn.rs @@ -0,0 +1,558 @@ +use std::{ + any::Any, + fmt::Debug, + pin::Pin, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, + }, +}; + +use futures::{StreamExt, TryFutureExt}; + +use prost::Message; + +use tokio::{ + sync::{broadcast, mpsc, Mutex}, + task::JoinSet, + time::{timeout, Duration}, +}; + +use tracing::Instrument; +use zerocopy::AsBytes; + +use crate::{ + common::{ + config::{NetworkIdentity, NetworkSecretDigest}, + defer, + error::Error, + global_ctx::ArcGlobalCtx, + PeerId, + }, + proto::{ + cli::{PeerConnInfo, PeerConnStats}, + common::TunnelInfo, + peer_rpc::HandshakeRequest, + }, + tunnel::{ + filter::{StatsRecorderTunnelFilter, TunnelFilter, TunnelWithFilter}, + mpsc::{MpscTunnel, MpscTunnelSender}, + packet_def::{PacketType, ZCPacket}, + stats::{Throughput, WindowLatency}, + Tunnel, TunnelError, ZCPacketStream, + }, +}; + +use super::{peer_conn_ping::PeerConnPinger, PacketRecvChan}; + +pub type PeerConnId = uuid::Uuid; + +const MAGIC: u32 = 0xd1e1a5e1; +const VERSION: u32 = 1; + +pub struct PeerConn { + conn_id: PeerConnId, + + my_peer_id: PeerId, + global_ctx: ArcGlobalCtx, + + tunnel: Arc>>, + sink: MpscTunnelSender, + recv: Arc>>>>, + tunnel_info: Option, + + tasks: JoinSet>, + + info: Option, + is_client: Option, + + close_event_sender: Option>, + + ctrl_resp_sender: broadcast::Sender, + + latency_stats: Arc, + throughput: Arc, + loss_rate_stats: Arc, +} + +impl Debug for PeerConn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PeerConn") + .field("conn_id", &self.conn_id) + .field("my_peer_id", &self.my_peer_id) + .field("info", &self.info) + .finish() + } +} + +impl PeerConn { + pub fn new(my_peer_id: PeerId, global_ctx: ArcGlobalCtx, tunnel: Box) -> Self { + let tunnel_info = tunnel.info(); + let (ctrl_sender, _ctrl_receiver) = broadcast::channel(100); + + let peer_conn_tunnel_filter = StatsRecorderTunnelFilter::new(); + let throughput = peer_conn_tunnel_filter.filter_output(); + let peer_conn_tunnel = TunnelWithFilter::new(tunnel, peer_conn_tunnel_filter); + let mut mpsc_tunnel = MpscTunnel::new(peer_conn_tunnel, Some(Duration::from_secs(7))); + + let (recv, sink) = (mpsc_tunnel.get_stream(), mpsc_tunnel.get_sink()); + + PeerConn { + conn_id: PeerConnId::new_v4(), + + my_peer_id, + global_ctx, + + tunnel: Arc::new(Mutex::new(Box::new(defer::Defer::new(move || { + mpsc_tunnel.close() + })))), + sink, + recv: Arc::new(Mutex::new(Some(recv))), + tunnel_info, + + tasks: JoinSet::new(), + + info: None, + is_client: None, + close_event_sender: None, + + ctrl_resp_sender: ctrl_sender, + + latency_stats: Arc::new(WindowLatency::new(15)), + throughput, + loss_rate_stats: Arc::new(AtomicU32::new(0)), + } + } + + pub fn get_conn_id(&self) -> PeerConnId { + self.conn_id + } + + async fn wait_handshake(&mut self, need_retry: &mut bool) -> Result { + *need_retry = false; + + let mut locked = self.recv.lock().await; + let recv = locked.as_mut().unwrap(); + let rsp = match recv.next().await { + Some(Ok(rsp)) => rsp, + Some(Err(e)) => { + return Err(Error::WaitRespError(format!( + "conn recv error during wait handshake response, err: {:?}", + e + ))) + } + None => { + return Err(Error::WaitRespError( + "conn closed during wait handshake response".to_owned(), + )) + } + }; + + *need_retry = true; + + let Some(peer_mgr_hdr) = rsp.peer_manager_header() else { + return Err(Error::WaitRespError(format!( + "unexpected packet: {:?}, cannot decode peer manager hdr", + rsp + ))); + }; + + if peer_mgr_hdr.packet_type != PacketType::HandShake as u8 { + return Err(Error::WaitRespError(format!( + "unexpected packet type: {:?}", + peer_mgr_hdr.packet_type + ))); + } + + let rsp = HandshakeRequest::decode(rsp.payload()).map_err(|e| { + Error::WaitRespError(format!("decode handshake response error: {:?}", e)) + })?; + + if rsp.network_secret_digrest.len() != std::mem::size_of::() { + return Err(Error::WaitRespError( + "invalid network secret digest".to_owned(), + )); + } + + return Ok(rsp); + } + + async fn wait_handshake_loop(&mut self) -> Result { + timeout(Duration::from_secs(5), async move { + loop { + let mut need_retry = true; + match self.wait_handshake(&mut need_retry).await { + Ok(rsp) => return Ok(rsp), + Err(e) => { + tracing::warn!("wait handshake error: {:?}", e); + if !need_retry { + return Err(e); + } + } + } + } + }) + .map_err(|e| Error::WaitRespError(format!("wait handshake timeout: {:?}", e))) + .await? + } + + async fn send_handshake(&mut self) -> Result<(), Error> { + let network = self.global_ctx.get_network_identity(); + let mut req = HandshakeRequest { + magic: MAGIC, + my_peer_id: self.my_peer_id, + version: VERSION, + features: Vec::new(), + network_name: network.network_name.clone(), + ..Default::default() + }; + req.network_secret_digrest + .extend_from_slice(&network.network_secret_digest.unwrap_or_default()); + + let hs_req = req.encode_to_vec(); + let mut zc_packet = ZCPacket::new_with_payload(hs_req.as_bytes()); + zc_packet.fill_peer_manager_hdr( + self.my_peer_id, + PeerId::default(), + PacketType::HandShake as u8, + ); + + self.sink.send(zc_packet).await.map_err(|e| { + tracing::warn!("send handshake request error: {:?}", e); + Error::WaitRespError("send handshake request error".to_owned()) + })?; + + // yield to send the response packet + tokio::task::yield_now().await; + + Ok(()) + } + + #[tracing::instrument] + pub async fn do_handshake_as_server(&mut self) -> Result<(), Error> { + let rsp = self.wait_handshake_loop().await?; + tracing::info!("handshake request: {:?}", rsp); + self.info = Some(rsp); + self.is_client = Some(false); + self.send_handshake().await?; + + if self.get_peer_id() == self.my_peer_id { + Err(Error::WaitRespError("peer id conflict".to_owned())) + } else { + Ok(()) + } + } + + #[tracing::instrument] + pub async fn do_handshake_as_client(&mut self) -> Result<(), Error> { + self.send_handshake().await?; + tracing::info!("waiting for handshake request from server"); + let rsp = self.wait_handshake_loop().await?; + tracing::info!("handshake response: {:?}", rsp); + self.info = Some(rsp); + self.is_client = Some(true); + + if self.get_peer_id() == self.my_peer_id { + Err(Error::WaitRespError("peer id conflict".to_owned())) + } else { + Ok(()) + } + } + + pub fn handshake_done(&self) -> bool { + self.info.is_some() + } + + pub async fn start_recv_loop(&mut self, packet_recv_chan: PacketRecvChan) { + let mut stream = self.recv.lock().await.take().unwrap(); + let sink = self.sink.clone(); + let sender = packet_recv_chan.clone(); + let close_event_sender = self.close_event_sender.clone().unwrap(); + let conn_id = self.conn_id; + let ctrl_sender = self.ctrl_resp_sender.clone(); + let _conn_info = self.get_conn_info(); + let conn_info_for_instrument = self.get_conn_info(); + + self.tasks.spawn( + async move { + tracing::info!("start recving peer conn packet"); + let mut task_ret = Ok(()); + while let Some(ret) = stream.next().await { + if ret.is_err() { + tracing::error!(error = ?ret, "peer conn recv error"); + task_ret = Err(ret.err().unwrap()); + break; + } + + let mut zc_packet = ret.unwrap(); + let Some(peer_mgr_hdr) = zc_packet.mut_peer_manager_header() else { + tracing::error!( + "unexpected packet: {:?}, cannot decode peer manager hdr", + zc_packet + ); + continue; + }; + + if peer_mgr_hdr.packet_type == PacketType::Ping as u8 { + peer_mgr_hdr.packet_type = PacketType::Pong as u8; + if let Err(e) = sink.send(zc_packet).await { + tracing::error!(?e, "peer conn send req error"); + } + } else if peer_mgr_hdr.packet_type == PacketType::Pong as u8 { + if let Err(e) = ctrl_sender.send(zc_packet) { + tracing::error!(?e, "peer conn send ctrl resp error"); + } + } else { + if sender.send(zc_packet).await.is_err() { + break; + } + } + } + + tracing::info!("end recving peer conn packet"); + + drop(sink); + if let Err(e) = close_event_sender.send(conn_id).await { + tracing::error!(error = ?e, "peer conn close event send error"); + } + + task_ret + } + .instrument( + tracing::info_span!("peer conn recv loop", conn_info = ?conn_info_for_instrument), + ), + ); + } + + pub fn start_pingpong(&mut self) { + let mut pingpong = PeerConnPinger::new( + self.my_peer_id, + self.get_peer_id(), + self.sink.clone(), + self.ctrl_resp_sender.clone(), + self.latency_stats.clone(), + self.loss_rate_stats.clone(), + self.throughput.clone(), + ); + + let close_event_sender = self.close_event_sender.clone().unwrap(); + let conn_id = self.conn_id; + + self.tasks.spawn(async move { + pingpong.pingpong().await; + + tracing::warn!(?pingpong, "pingpong task exit"); + + if let Err(e) = close_event_sender.send(conn_id).await { + tracing::warn!("close event sender error: {:?}", e); + } + + Ok(()) + }); + } + + pub async fn send_msg(&self, msg: ZCPacket) -> Result<(), Error> { + Ok(self.sink.send(msg).await?) + } + + pub fn get_peer_id(&self) -> PeerId { + self.info.as_ref().unwrap().my_peer_id + } + + pub fn get_network_identity(&self) -> NetworkIdentity { + let info = self.info.as_ref().unwrap(); + let mut ret = NetworkIdentity { + network_name: info.network_name.clone(), + ..Default::default() + }; + ret.network_secret_digest = Some([0u8; 32]); + ret.network_secret_digest + .as_mut() + .unwrap() + .copy_from_slice(&info.network_secret_digrest); + ret + } + + pub fn set_close_event_sender(&mut self, sender: mpsc::Sender) { + self.close_event_sender = Some(sender); + } + + pub fn get_stats(&self) -> PeerConnStats { + PeerConnStats { + latency_us: self.latency_stats.get_latency_us(), + + tx_bytes: self.throughput.tx_bytes(), + rx_bytes: self.throughput.rx_bytes(), + + tx_packets: self.throughput.tx_packets(), + rx_packets: self.throughput.rx_packets(), + } + } + + pub fn get_conn_info(&self) -> PeerConnInfo { + let info = self.info.as_ref().unwrap(); + PeerConnInfo { + conn_id: self.conn_id.to_string(), + my_peer_id: self.my_peer_id, + peer_id: self.get_peer_id(), + features: info.features.clone(), + tunnel: self.tunnel_info.clone(), + stats: Some(self.get_stats()), + loss_rate: (f64::from(self.loss_rate_stats.load(Ordering::Relaxed)) / 100.0) as f32, + is_client: self.is_client.unwrap_or_default(), + network_name: info.network_name.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use crate::common::global_ctx::tests::get_mock_global_ctx; + use crate::common::new_peer_id; + use crate::common::scoped_task::ScopedTask; + use crate::peers::create_packet_recv_chan; + use crate::tunnel::filter::tests::DropSendTunnelFilter; + use crate::tunnel::filter::PacketRecorderTunnelFilter; + use crate::tunnel::ring::create_ring_tunnel_pair; + + #[tokio::test] + async fn peer_conn_handshake_same_id() { + let (c, s) = create_ring_tunnel_pair(); + let c_peer_id = new_peer_id(); + let s_peer_id = c_peer_id; + + let mut c_peer = PeerConn::new(c_peer_id, get_mock_global_ctx(), Box::new(c)); + let mut s_peer = PeerConn::new(s_peer_id, get_mock_global_ctx(), Box::new(s)); + + let (c_ret, s_ret) = tokio::join!( + c_peer.do_handshake_as_client(), + s_peer.do_handshake_as_server() + ); + + assert!(c_ret.is_err()); + assert!(s_ret.is_err()); + } + + #[tokio::test] + async fn peer_conn_handshake() { + let (c, s) = create_ring_tunnel_pair(); + + let c_recorder = Arc::new(PacketRecorderTunnelFilter::new()); + let s_recorder = Arc::new(PacketRecorderTunnelFilter::new()); + + let c = TunnelWithFilter::new(c, c_recorder.clone()); + let s = TunnelWithFilter::new(s, s_recorder.clone()); + + let c_peer_id = new_peer_id(); + let s_peer_id = new_peer_id(); + + let mut c_peer = PeerConn::new(c_peer_id, get_mock_global_ctx(), Box::new(c)); + + let mut s_peer = PeerConn::new(s_peer_id, get_mock_global_ctx(), Box::new(s)); + + let (c_ret, s_ret) = tokio::join!( + c_peer.do_handshake_as_client(), + s_peer.do_handshake_as_server() + ); + + c_ret.unwrap(); + s_ret.unwrap(); + + assert_eq!(c_recorder.sent.lock().unwrap().len(), 1); + assert_eq!(c_recorder.received.lock().unwrap().len(), 1); + + assert_eq!(s_recorder.sent.lock().unwrap().len(), 1); + assert_eq!(s_recorder.received.lock().unwrap().len(), 1); + + assert_eq!(c_peer.get_peer_id(), s_peer_id); + assert_eq!(s_peer.get_peer_id(), c_peer_id); + assert_eq!(c_peer.get_network_identity(), s_peer.get_network_identity()); + assert_eq!(c_peer.get_network_identity(), NetworkIdentity::default()); + } + + async fn peer_conn_pingpong_test_common( + drop_start: u32, + drop_end: u32, + conn_closed: bool, + drop_both: bool, + ) { + let (c, s) = create_ring_tunnel_pair(); + + // drop 1-3 packets should not affect pingpong + let c_recorder = Arc::new(DropSendTunnelFilter::new(drop_start, drop_end)); + let c = TunnelWithFilter::new(c, c_recorder.clone()); + + let c_peer_id = new_peer_id(); + let s_peer_id = new_peer_id(); + + let mut c_peer = PeerConn::new(c_peer_id, get_mock_global_ctx(), Box::new(c)); + let mut s_peer = PeerConn::new(s_peer_id, get_mock_global_ctx(), Box::new(s)); + + let (c_ret, s_ret) = tokio::join!( + c_peer.do_handshake_as_client(), + s_peer.do_handshake_as_server() + ); + + s_peer.set_close_event_sender(tokio::sync::mpsc::channel(1).0); + s_peer.start_recv_loop(create_packet_recv_chan().0).await; + // do not start ping for s, s only reponde to ping from c + + assert!(c_ret.is_ok()); + assert!(s_ret.is_ok()); + + let (close_send, mut close_recv) = tokio::sync::mpsc::channel(1); + c_peer.set_close_event_sender(close_send); + c_peer.start_pingpong(); + c_peer.start_recv_loop(create_packet_recv_chan().0).await; + + let throughput = c_peer.throughput.clone(); + let _t = ScopedTask::from(tokio::spawn(async move { + // if not drop both, we mock some rx traffic for client peer to test pinger + while !drop_both { + tokio::time::sleep(Duration::from_millis(100)).await; + throughput.record_rx_bytes(3); + } + })); + + tokio::time::sleep(Duration::from_secs(15)).await; + + if conn_closed { + assert!(close_recv.try_recv().is_ok()); + } else { + assert!(close_recv.try_recv().is_err()); + } + } + + #[tokio::test] + async fn peer_conn_pingpong_timeout_not_close() { + peer_conn_pingpong_test_common(3, 5, false, false).await; + } + + #[tokio::test] + async fn peer_conn_pingpong_oneside_timeout() { + peer_conn_pingpong_test_common(4, 12, false, false).await; + } + + #[tokio::test] + async fn peer_conn_pingpong_bothside_timeout() { + peer_conn_pingpong_test_common(3, 14, true, true).await; + } + + #[tokio::test] + async fn close_tunnel_during_handshake() { + let (c, s) = create_ring_tunnel_pair(); + let mut c_peer = PeerConn::new(new_peer_id(), get_mock_global_ctx(), Box::new(c)); + let j = tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(1)).await; + drop(s); + }); + timeout(Duration::from_millis(1500), c_peer.do_handshake_as_client()) + .await + .unwrap() + .unwrap_err(); + let _ = tokio::join!(j); + } +} diff --git a/rust/easytier/src/peers/peer_conn_ping.rs b/rust/easytier/src/peers/peer_conn_ping.rs new file mode 100644 index 0000000..93e0afb --- /dev/null +++ b/rust/easytier/src/peers/peer_conn_ping.rs @@ -0,0 +1,349 @@ +use std::{ + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, + }, + time::Duration, +}; + +use rand::{thread_rng, Rng}; +use tokio::{ + sync::broadcast, + task::JoinSet, + time::{timeout, Interval}, +}; +use tracing::Instrument; + +use crate::{ + common::{error::Error, PeerId}, + tunnel::{ + mpsc::MpscTunnelSender, + packet_def::{PacketType, ZCPacket}, + stats::{Throughput, WindowLatency}, + TunnelError, + }, +}; + +struct PingIntervalController { + throughput: Arc, + loss_counter: Arc, + + interval: Interval, + + logic_time: u64, + last_send_logic_time: u64, + + backoff_idx: i32, + max_backoff_idx: i32, + + last_throughput: Throughput, +} + +impl std::fmt::Debug for PingIntervalController { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PingIntervalController") + .field("throughput", &self.throughput) + .field("loss_counter", &self.loss_counter) + .field("logic_time", &self.logic_time) + .field("last_send_logic_time", &self.last_send_logic_time) + .field("backoff_idx", &self.backoff_idx) + .field("max_backoff_idx", &self.max_backoff_idx) + .field("last_throughput", &self.last_throughput) + .finish() + } +} + +impl PingIntervalController { + fn new(throughput: Arc, loss_counter: Arc) -> Self { + let last_throughput = *throughput; + + Self { + throughput, + loss_counter, + interval: tokio::time::interval(Duration::from_secs(1)), + logic_time: 0, + last_send_logic_time: 0, + + backoff_idx: 0, + max_backoff_idx: 5, + + last_throughput, + } + } + + async fn tick(&mut self) { + self.interval.tick().await; + self.logic_time += 1; + } + + fn tx_increase(&self) -> bool { + self.throughput.tx_packets() > self.last_throughput.tx_packets() + } + + fn rx_increase(&self) -> bool { + self.throughput.rx_packets() > self.last_throughput.rx_packets() + } + + fn should_send_ping(&mut self) -> bool { + if self.loss_counter.load(Ordering::Relaxed) > 0 { + self.backoff_idx = 0; + } else if self.tx_increase() && !self.rx_increase() { + // if tx increase but rx not increase, we should do pingpong more frequently + self.backoff_idx = 0; + } + + self.last_throughput = *self.throughput; + + if (self.logic_time - self.last_send_logic_time) < (1 << self.backoff_idx) { + return false; + } + + self.backoff_idx = std::cmp::min(self.backoff_idx + 1, self.max_backoff_idx); + + // use this makes two peers not pingpong at the same time + if self.backoff_idx > self.max_backoff_idx - 2 && thread_rng().gen_bool(0.2) { + self.backoff_idx -= 1; + } + + self.last_send_logic_time = self.logic_time; + return true; + } +} + +pub struct PeerConnPinger { + my_peer_id: PeerId, + peer_id: PeerId, + sink: MpscTunnelSender, + ctrl_sender: broadcast::Sender, + latency_stats: Arc, + loss_rate_stats: Arc, + throughput_stats: Arc, + tasks: JoinSet>, +} + +impl std::fmt::Debug for PeerConnPinger { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PeerConnPinger") + .field("my_peer_id", &self.my_peer_id) + .field("peer_id", &self.peer_id) + .finish() + } +} + +impl PeerConnPinger { + pub fn new( + my_peer_id: PeerId, + peer_id: PeerId, + sink: MpscTunnelSender, + ctrl_sender: broadcast::Sender, + latency_stats: Arc, + loss_rate_stats: Arc, + throughput_stats: Arc, + ) -> Self { + Self { + my_peer_id, + peer_id, + sink, + tasks: JoinSet::new(), + latency_stats, + ctrl_sender, + loss_rate_stats, + throughput_stats, + } + } + + fn new_ping_packet(my_node_id: PeerId, peer_id: PeerId, seq: u32) -> ZCPacket { + let mut packet = ZCPacket::new_with_payload(&seq.to_le_bytes()); + packet.fill_peer_manager_hdr(my_node_id, peer_id, PacketType::Ping as u8); + packet + } + + async fn do_pingpong_once( + my_node_id: PeerId, + peer_id: PeerId, + sink: &mut MpscTunnelSender, + receiver: &mut broadcast::Receiver, + seq: u32, + ) -> Result { + // should add seq here. so latency can be calculated more accurately + let req = Self::new_ping_packet(my_node_id, peer_id, seq); + sink.send(req).await?; + + let now = std::time::Instant::now(); + // wait until we get a pong packet in ctrl_resp_receiver + let resp = timeout(Duration::from_secs(2), async { + loop { + match receiver.recv().await { + Ok(p) => { + let payload = p.payload(); + let Ok(seq_buf) = payload[0..4].try_into() else { + tracing::debug!("pingpong recv invalid packet, continue"); + continue; + }; + let resp_seq = u32::from_le_bytes(seq_buf); + if resp_seq == seq { + break; + } + } + Err(e) => { + return Err(Error::WaitRespError(format!( + "wait ping response error: {:?}", + e + ))); + } + } + } + Ok(()) + }) + .await; + + tracing::trace!(?resp, "wait ping response done"); + + if resp.is_err() { + return Err(Error::WaitRespError( + "wait ping response timeout".to_owned(), + )); + } + + if resp.as_ref().unwrap().is_err() { + return Err(resp.unwrap().err().unwrap()); + } + + Ok(now.elapsed().as_micros()) + } + + pub async fn pingpong(&mut self) { + let sink = self.sink.clone(); + let my_node_id = self.my_peer_id; + let peer_id = self.peer_id; + let latency_stats = self.latency_stats.clone(); + + let (ping_res_sender, mut ping_res_receiver) = tokio::sync::mpsc::channel(100); + + // one with 1% precision + let loss_rate_stats_1 = WindowLatency::new(100); + // disconnect the connection if lost 5 pingpong consecutively + let loss_counter = Arc::new(AtomicU32::new(0)); + + let stopped = Arc::new(AtomicU32::new(0)); + + // generate a pingpong task every 200ms + let mut pingpong_tasks = JoinSet::new(); + let ctrl_resp_sender = self.ctrl_sender.clone(); + let stopped_clone = stopped.clone(); + let mut controller = + PingIntervalController::new(self.throughput_stats.clone(), loss_counter.clone()); + self.tasks.spawn( + async move { + let mut req_seq = 0; + loop { + controller.tick().await; + + if stopped_clone.load(Ordering::Relaxed) != 0 { + return Ok(()); + } + + while pingpong_tasks.len() > 5 { + pingpong_tasks.join_next().await; + } + + if !controller.should_send_ping() { + continue; + } + + tracing::debug!( + "pingpong controller send pingpong task, seq: {}, node_id: {}, controller: {:?}", + req_seq, + my_node_id, + controller + ); + + let mut sink = sink.clone(); + let receiver = ctrl_resp_sender.subscribe(); + let ping_res_sender = ping_res_sender.clone(); + pingpong_tasks.spawn(async move { + let mut receiver = receiver.resubscribe(); + let pingpong_once_ret = Self::do_pingpong_once( + my_node_id, + peer_id, + &mut sink, + &mut receiver, + req_seq, + ) + .await; + + if let Err(e) = ping_res_sender.send(pingpong_once_ret).await { + tracing::info!(?e, "pingpong task send result error, exit.."); + }; + }); + + req_seq = req_seq.wrapping_add(1); + } + } + .instrument(tracing::info_span!( + "pingpong_controller", + ?my_node_id, + ?peer_id + )), + ); + + let throughput = self.throughput_stats.clone(); + let mut last_rx_packets = throughput.rx_packets(); + + while let Some(ret) = ping_res_receiver.recv().await { + if let Ok(lat) = ret { + latency_stats.record_latency(lat as u32); + + loss_rate_stats_1.record_latency(0); + } else { + loss_rate_stats_1.record_latency(1); + loss_counter.fetch_add(1, Ordering::Relaxed); + } + + let loss_rate_1: f64 = loss_rate_stats_1.get_latency_us(); + + tracing::trace!( + ?ret, + ?self, + ?loss_rate_1, + "pingpong task recv pingpong_once result" + ); + + let current_rx_packets = throughput.rx_packets(); + if last_rx_packets != current_rx_packets { + // if we receive some packet from peers, reset the counter to avoid conn close. + // conn will close only if we have 5 continous round pingpong loss after no packet received. + loss_counter.store(0, Ordering::Relaxed); + } + + tracing::debug!( + "loss_counter: {:?}, loss_rate_1: {}, cur_rx_packets: {}, last_rx: {}, node_id: {}", + loss_counter, + loss_rate_1, + current_rx_packets, + last_rx_packets, + my_node_id + ); + + if loss_counter.load(Ordering::Relaxed) >= 5 { + tracing::warn!( + ?ret, + ?self, + ?loss_rate_1, + ?loss_counter, + ?last_rx_packets, + ?current_rx_packets, + "pingpong loss too much pingpong packet and no other ingress packets, closing the connection", + ); + break; + } + + last_rx_packets = throughput.rx_packets(); + self.loss_rate_stats + .store((loss_rate_1 * 100.0) as u32, Ordering::Relaxed); + } + + stopped.store(1, Ordering::Relaxed); + ping_res_receiver.close(); + } +} diff --git a/rust/easytier/src/peers/peer_manager.rs b/rust/easytier/src/peers/peer_manager.rs new file mode 100644 index 0000000..e6b7f0a --- /dev/null +++ b/rust/easytier/src/peers/peer_manager.rs @@ -0,0 +1,1214 @@ +use std::{ + fmt::Debug, + net::Ipv4Addr, + sync::{Arc, Weak}, + time::SystemTime, +}; + +use anyhow::Context; +use async_trait::async_trait; + +use dashmap::DashMap; + +use tokio::{ + sync::{ + mpsc::{self, UnboundedReceiver, UnboundedSender}, + Mutex, RwLock, + }, + task::JoinSet, +}; + +use crate::{ + common::{ + compressor::{Compressor as _, DefaultCompressor}, + constants::EASYTIER_VERSION, + error::Error, + global_ctx::{ArcGlobalCtx, NetworkIdentity}, + stun::StunInfoCollectorTrait, + PeerId, + }, + peers::{ + peer_conn::PeerConn, + peer_rpc::PeerRpcManagerTransport, + recv_packet_from_chan, + route_trait::{ForeignNetworkRouteInfoMap, NextHopPolicy, RouteInterface}, + PeerPacketFilter, + }, + proto::{ + cli::{ + self, list_global_foreign_network_response::OneForeignNetwork, + ListGlobalForeignNetworkResponse, + }, + peer_rpc::{ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey}, + }, + tunnel::{ + self, + packet_def::{CompressorAlgo, PacketType, ZCPacket}, + Tunnel, TunnelConnector, + }, +}; + +use super::{ + create_packet_recv_chan, + encrypt::{Encryptor, NullCipher}, + foreign_network_client::ForeignNetworkClient, + foreign_network_manager::{ForeignNetworkManager, GlobalForeignNetworkAccessor}, + peer_conn::PeerConnId, + peer_map::PeerMap, + peer_ospf_route::PeerRoute, + peer_rpc::PeerRpcManager, + route_trait::{ArcRoute, Route}, + BoxNicPacketFilter, BoxPeerPacketFilter, PacketRecvChan, PacketRecvChanReceiver, +}; + +struct RpcTransport { + my_peer_id: PeerId, + peers: Weak, + // TODO: this seems can be removed + foreign_peers: Mutex>>, + + packet_recv: Mutex>, + peer_rpc_tspt_sender: UnboundedSender, + + encryptor: Arc>, +} + +#[async_trait::async_trait] +impl PeerRpcManagerTransport for RpcTransport { + fn my_peer_id(&self) -> PeerId { + self.my_peer_id + } + + async fn send(&self, mut msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error> { + let peers = self.peers.upgrade().ok_or(Error::Unknown)?; + // NOTE: if route info is not exchanged, this will return error. treat it as need relay + if !peers + .need_relay_by_foreign_network(dst_peer_id) + .await + .unwrap_or(true) + { + self.encryptor + .encrypt(&mut msg) + .with_context(|| "encrypt failed")?; + } + // send to self and this packet will be forwarded in peer_recv loop + peers.send_msg_directly(msg, self.my_peer_id).await + } + + async fn recv(&self) -> Result { + if let Some(o) = self.packet_recv.lock().await.recv().await { + Ok(o) + } else { + Err(Error::Unknown) + } + } +} + +pub enum RouteAlgoType { + Ospf, + None, +} + +enum RouteAlgoInst { + Ospf(Arc), + None, +} + +pub struct PeerManager { + my_peer_id: PeerId, + + global_ctx: ArcGlobalCtx, + nic_channel: PacketRecvChan, + + tasks: Arc>>, + + packet_recv: Arc>>, + + peers: Arc, + + peer_rpc_mgr: Arc, + peer_rpc_tspt: Arc, + + peer_packet_process_pipeline: Arc>>, + nic_packet_process_pipeline: Arc>>, + + route_algo_inst: RouteAlgoInst, + + foreign_network_manager: Arc, + foreign_network_client: Arc, + + encryptor: Arc>, + data_compress_algo: CompressorAlgo, + + exit_nodes: Vec, +} + +impl Debug for PeerManager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PeerManager") + .field("my_peer_id", &self.my_peer_id()) + .field("instance_name", &self.global_ctx.inst_name) + .field("net_ns", &self.global_ctx.net_ns.name()) + .finish() + } +} + +impl PeerManager { + pub fn new( + route_algo: RouteAlgoType, + global_ctx: ArcGlobalCtx, + nic_channel: PacketRecvChan, + ) -> Self { + let my_peer_id = rand::random(); + + let (packet_send, packet_recv) = create_packet_recv_chan(); + let peers = Arc::new(PeerMap::new( + packet_send.clone(), + global_ctx.clone(), + my_peer_id, + )); + + let mut encryptor: Arc> = Arc::new(Box::new(NullCipher)); + if global_ctx.get_flags().enable_encryption { + #[cfg(feature = "wireguard")] + { + use super::encrypt::ring_aes_gcm::AesGcmCipher; + encryptor = Arc::new(Box::new(AesGcmCipher::new_128(global_ctx.get_128_key()))); + } + + #[cfg(all(feature = "aes-gcm", not(feature = "wireguard")))] + { + use super::encrypt::aes_gcm::AesGcmCipher; + encryptor = Arc::new(Box::new(AesGcmCipher::new_128(global_ctx.get_128_key()))); + } + + #[cfg(all(not(feature = "wireguard"), not(feature = "aes-gcm")))] + { + compile_error!("wireguard or aes-gcm feature must be enabled for encryption"); + } + } + + if global_ctx + .check_network_in_whitelist(&global_ctx.get_network_name()) + .is_err() + { + // if local network is not in whitelist, avoid relay data when exist any other route path + let mut f = global_ctx.get_feature_flags(); + f.avoid_relay_data = true; + global_ctx.set_feature_flags(f); + } + + // TODO: remove these because we have impl pipeline processor. + let (peer_rpc_tspt_sender, peer_rpc_tspt_recv) = mpsc::unbounded_channel(); + let rpc_tspt = Arc::new(RpcTransport { + my_peer_id, + peers: Arc::downgrade(&peers), + foreign_peers: Mutex::new(None), + packet_recv: Mutex::new(peer_rpc_tspt_recv), + peer_rpc_tspt_sender, + encryptor: encryptor.clone(), + }); + let peer_rpc_mgr = Arc::new(PeerRpcManager::new(rpc_tspt.clone())); + + let route_algo_inst = match route_algo { + RouteAlgoType::Ospf => RouteAlgoInst::Ospf(PeerRoute::new( + my_peer_id, + global_ctx.clone(), + peer_rpc_mgr.clone(), + )), + RouteAlgoType::None => RouteAlgoInst::None, + }; + + let foreign_network_manager = Arc::new(ForeignNetworkManager::new( + my_peer_id, + global_ctx.clone(), + packet_send.clone(), + Self::build_foreign_network_manager_accessor(&peers), + )); + let foreign_network_client = Arc::new(ForeignNetworkClient::new( + global_ctx.clone(), + packet_send.clone(), + peer_rpc_mgr.clone(), + my_peer_id, + )); + + let data_compress_algo = global_ctx + .get_flags() + .data_compress_algo() + .try_into() + .expect("invalid data compress algo, maybe some features not enabled"); + + let exit_nodes = global_ctx.config.get_exit_nodes(); + + PeerManager { + my_peer_id, + + global_ctx, + nic_channel, + + tasks: Arc::new(Mutex::new(JoinSet::new())), + + packet_recv: Arc::new(Mutex::new(Some(packet_recv))), + + peers: peers.clone(), + + peer_rpc_mgr, + peer_rpc_tspt: rpc_tspt, + + peer_packet_process_pipeline: Arc::new(RwLock::new(Vec::new())), + nic_packet_process_pipeline: Arc::new(RwLock::new(Vec::new())), + + route_algo_inst, + + foreign_network_manager, + foreign_network_client, + + encryptor, + data_compress_algo, + + exit_nodes, + } + } + + fn build_foreign_network_manager_accessor( + peer_map: &Arc, + ) -> Box { + struct T { + peer_map: Weak, + } + + #[async_trait::async_trait] + impl GlobalForeignNetworkAccessor for T { + async fn list_global_foreign_peer( + &self, + network_identity: &NetworkIdentity, + ) -> Vec { + let Some(peer_map) = self.peer_map.upgrade() else { + return vec![]; + }; + + peer_map + .list_peers_own_foreign_network(network_identity) + .await + } + } + + Box::new(T { + peer_map: Arc::downgrade(peer_map), + }) + } + + async fn add_new_peer_conn(&self, peer_conn: PeerConn) -> Result<(), Error> { + if self.global_ctx.get_network_identity() != peer_conn.get_network_identity() { + return Err(Error::SecretKeyError( + "network identity not match".to_string(), + )); + } + Ok(self.peers.add_new_peer_conn(peer_conn).await) + } + + pub async fn add_client_tunnel( + &self, + tunnel: Box, + ) -> Result<(PeerId, PeerConnId), Error> { + let mut peer = PeerConn::new(self.my_peer_id, self.global_ctx.clone(), tunnel); + peer.do_handshake_as_client().await?; + let conn_id = peer.get_conn_id(); + let peer_id = peer.get_peer_id(); + if peer.get_network_identity().network_name + == self.global_ctx.get_network_identity().network_name + { + self.add_new_peer_conn(peer).await?; + } else { + self.foreign_network_client.add_new_peer_conn(peer).await; + } + Ok((peer_id, conn_id)) + } + + #[tracing::instrument] + pub async fn try_connect(&self, mut connector: C) -> Result<(PeerId, PeerConnId), Error> + where + C: TunnelConnector + Debug, + { + let ns = self.global_ctx.net_ns.clone(); + let t = ns + .run_async(|| async move { connector.connect().await }) + .await?; + self.add_client_tunnel(t).await + } + + #[tracing::instrument] + pub async fn add_tunnel_as_server(&self, tunnel: Box) -> Result<(), Error> { + tracing::info!("add tunnel as server start"); + let mut peer = PeerConn::new(self.my_peer_id, self.global_ctx.clone(), tunnel); + peer.do_handshake_as_server().await?; + if peer.get_network_identity().network_name + == self.global_ctx.get_network_identity().network_name + { + self.add_new_peer_conn(peer).await?; + } else { + self.foreign_network_manager.add_peer_conn(peer).await?; + } + tracing::info!("add tunnel as server done"); + Ok(()) + } + + async fn try_handle_foreign_network_packet( + packet: ZCPacket, + my_peer_id: PeerId, + peer_map: &PeerMap, + foreign_network_mgr: &ForeignNetworkManager, + ) -> Result<(), ZCPacket> { + let pm_header = packet.peer_manager_header().unwrap(); + if pm_header.packet_type != PacketType::ForeignNetworkPacket as u8 { + return Err(packet); + } + + let from_peer_id = pm_header.from_peer_id.get(); + let to_peer_id = pm_header.to_peer_id.get(); + + let foreign_hdr = packet.foreign_network_hdr().unwrap(); + let foreign_network_name = foreign_hdr.get_network_name(packet.payload()); + let foreign_peer_id = foreign_hdr.get_dst_peer_id(); + + if to_peer_id == my_peer_id { + // packet sent from other peer to me, extract the inner packet and forward it + if let Err(e) = foreign_network_mgr + .send_msg_to_peer( + &foreign_network_name, + foreign_peer_id, + packet.foreign_network_packet(), + ) + .await + { + tracing::debug!( + ?e, + ?foreign_network_name, + ?foreign_peer_id, + "foreign network mgr send_msg_to_peer failed" + ); + } + Ok(()) + } else if from_peer_id == my_peer_id { + // packet is generated from foreign network mgr and should be forward to other peer + if let Err(e) = peer_map + .send_msg(packet, to_peer_id, NextHopPolicy::LeastHop) + .await + { + tracing::debug!( + ?e, + ?to_peer_id, + "send_msg_directly failed when forward local generated foreign network packet" + ); + } + + Ok(()) + } else { + // target is not me, forward it + Err(packet) + } + } + + async fn start_peer_recv(&self) { + let mut recv = self.packet_recv.lock().await.take().unwrap(); + let my_peer_id = self.my_peer_id; + let peers = self.peers.clone(); + let pipe_line = self.peer_packet_process_pipeline.clone(); + let foreign_client = self.foreign_network_client.clone(); + let foreign_mgr = self.foreign_network_manager.clone(); + let encryptor = self.encryptor.clone(); + let compress_algo = self.data_compress_algo; + self.tasks.lock().await.spawn(async move { + tracing::trace!("start_peer_recv"); + while let Ok(ret) = recv_packet_from_chan(&mut recv).await { + let Err(mut ret) = + Self::try_handle_foreign_network_packet(ret, my_peer_id, &peers, &foreign_mgr) + .await + else { + continue; + }; + + let Some(hdr) = ret.mut_peer_manager_header() else { + tracing::warn!(?ret, "invalid packet, skip"); + continue; + }; + + tracing::trace!(?hdr, "peer recv a packet..."); + let from_peer_id = hdr.from_peer_id.get(); + let to_peer_id = hdr.to_peer_id.get(); + if to_peer_id != my_peer_id { + if hdr.forward_counter > 7 { + tracing::warn!(?hdr, "forward counter exceed, drop packet"); + continue; + } + + if hdr.forward_counter > 2 && hdr.is_latency_first() { + tracing::trace!(?hdr, "set_latency_first false because too many hop"); + hdr.set_latency_first(false); + } + + hdr.forward_counter += 1; + + if from_peer_id == my_peer_id + && (hdr.packet_type == PacketType::Data as u8 + || hdr.packet_type == PacketType::KcpSrc as u8 + || hdr.packet_type == PacketType::KcpDst as u8) + { + let _ = Self::try_compress_and_encrypt(compress_algo, &encryptor, &mut ret) + .await; + } + + tracing::trace!(?to_peer_id, ?my_peer_id, "need forward"); + let ret = + Self::send_msg_internal(&peers, &foreign_client, ret, to_peer_id).await; + if ret.is_err() { + tracing::error!(?ret, ?to_peer_id, ?from_peer_id, "forward packet error"); + } + } else { + if let Err(e) = encryptor.decrypt(&mut ret) { + tracing::error!(?e, "decrypt failed"); + continue; + } + + let compressor = DefaultCompressor {}; + if let Err(e) = compressor.decompress(&mut ret).await { + tracing::error!(?e, "decompress failed"); + continue; + } + + let mut processed = false; + let mut zc_packet = Some(ret); + let mut idx = 0; + for pipeline in pipe_line.read().await.iter().rev() { + tracing::trace!(?zc_packet, ?idx, "try_process_packet_from_peer"); + idx += 1; + zc_packet = pipeline + .try_process_packet_from_peer(zc_packet.unwrap()) + .await; + if zc_packet.is_none() { + processed = true; + break; + } + } + if !processed { + tracing::error!(?zc_packet, "unhandled packet"); + } + } + } + panic!("done_peer_recv"); + }); + } + + pub async fn add_packet_process_pipeline(&self, pipeline: BoxPeerPacketFilter) { + // newest pipeline will be executed first + self.peer_packet_process_pipeline + .write() + .await + .push(pipeline); + } + + pub async fn add_nic_packet_process_pipeline(&self, pipeline: BoxNicPacketFilter) { + // newest pipeline will be executed first + self.nic_packet_process_pipeline + .write() + .await + .push(pipeline); + } + + async fn init_packet_process_pipeline(&self) { + // for tun/tap ip/eth packet. + struct NicPacketProcessor { + nic_channel: PacketRecvChan, + } + #[async_trait::async_trait] + impl PeerPacketFilter for NicPacketProcessor { + async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option { + let hdr = packet.peer_manager_header().unwrap(); + if hdr.packet_type == PacketType::Data as u8 { + tracing::trace!(?packet, "send packet to nic channel"); + // TODO: use a function to get the body ref directly for zero copy + let _ = self.nic_channel.send(packet).await; + None + } else { + Some(packet) + } + } + } + self.add_packet_process_pipeline(Box::new(NicPacketProcessor { + nic_channel: self.nic_channel.clone(), + })) + .await; + + // for peer rpc packet + struct PeerRpcPacketProcessor { + peer_rpc_tspt_sender: UnboundedSender, + } + + #[async_trait::async_trait] + impl PeerPacketFilter for PeerRpcPacketProcessor { + async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option { + let hdr = packet.peer_manager_header().unwrap(); + if hdr.packet_type == PacketType::TaRpc as u8 + || hdr.packet_type == PacketType::RpcReq as u8 + || hdr.packet_type == PacketType::RpcResp as u8 + { + self.peer_rpc_tspt_sender.send(packet).unwrap(); + None + } else { + Some(packet) + } + } + } + self.add_packet_process_pipeline(Box::new(PeerRpcPacketProcessor { + peer_rpc_tspt_sender: self.peer_rpc_tspt.peer_rpc_tspt_sender.clone(), + })) + .await; + } + + pub async fn add_route(&self, route: T) + where + T: Route + PeerPacketFilter + Send + Sync + Clone + 'static, + { + // for route + self.add_packet_process_pipeline(Box::new(route.clone())) + .await; + + struct Interface { + my_peer_id: PeerId, + peers: Weak, + foreign_network_client: Weak, + foreign_network_manager: Weak, + } + + #[async_trait] + impl RouteInterface for Interface { + async fn list_peers(&self) -> Vec { + let Some(foreign_client) = self.foreign_network_client.upgrade() else { + return vec![]; + }; + + let Some(peer_map) = self.peers.upgrade() else { + return vec![]; + }; + + let mut peers = foreign_client.list_public_peers().await; + peers.extend(peer_map.list_peers_with_conn().await); + peers + } + + fn my_peer_id(&self) -> PeerId { + self.my_peer_id + } + + async fn list_foreign_networks(&self) -> ForeignNetworkRouteInfoMap { + let ret = DashMap::new(); + let Some(foreign_mgr) = self.foreign_network_manager.upgrade() else { + return ret; + }; + + let networks = foreign_mgr.list_foreign_networks().await; + for (network_name, info) in networks.foreign_networks.iter() { + if info.peers.is_empty() { + continue; + } + + let last_update = foreign_mgr + .get_foreign_network_last_update(network_name) + .unwrap_or(SystemTime::now()); + ret.insert( + ForeignNetworkRouteInfoKey { + peer_id: self.my_peer_id, + network_name: network_name.clone(), + }, + ForeignNetworkRouteInfoEntry { + foreign_peer_ids: info.peers.iter().map(|x| x.peer_id).collect(), + last_update: Some(last_update.into()), + version: 0, + network_secret_digest: info.network_secret_digest.clone(), + }, + ); + } + ret + } + } + + let my_peer_id = self.my_peer_id; + let _route_id = route + .open(Box::new(Interface { + my_peer_id, + peers: Arc::downgrade(&self.peers), + foreign_network_client: Arc::downgrade(&self.foreign_network_client), + foreign_network_manager: Arc::downgrade(&self.foreign_network_manager), + })) + .await + .unwrap(); + + let arc_route: ArcRoute = Arc::new(Box::new(route)); + self.peers.add_route(arc_route).await; + } + + pub fn get_route(&self) -> Box { + match &self.route_algo_inst { + RouteAlgoInst::Ospf(route) => Box::new(route.clone()), + RouteAlgoInst::None => panic!("no route"), + } + } + + pub async fn list_routes(&self) -> Vec { + self.get_route().list_routes().await + } + + pub async fn dump_route(&self) -> String { + self.get_route().dump().await + } + + pub async fn list_global_foreign_network(&self) -> ListGlobalForeignNetworkResponse { + let mut resp = ListGlobalForeignNetworkResponse::default(); + let ret = self.get_route().list_foreign_network_info().await; + for info in ret.infos.iter() { + let entry = resp + .foreign_networks + .entry(info.key.as_ref().unwrap().peer_id) + .or_insert_with(|| Default::default()); + + let mut f = OneForeignNetwork::default(); + f.network_name = info.key.as_ref().unwrap().network_name.clone(); + f.peer_ids + .extend(info.value.as_ref().unwrap().foreign_peer_ids.iter()); + f.last_updated = format!("{}", info.value.as_ref().unwrap().last_update.unwrap()); + f.version = info.value.as_ref().unwrap().version; + + entry.foreign_networks.push(f); + } + + resp + } + + async fn run_nic_packet_process_pipeline(&self, data: &mut ZCPacket) { + for pipeline in self.nic_packet_process_pipeline.read().await.iter().rev() { + let _ = pipeline.try_process_packet_from_nic(data).await; + } + } + + fn get_next_hop_policy(is_first_latency: bool) -> NextHopPolicy { + if is_first_latency { + NextHopPolicy::LeastCost + } else { + NextHopPolicy::LeastHop + } + } + + pub async fn send_msg(&self, msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error> { + Self::send_msg_internal(&self.peers, &self.foreign_network_client, msg, dst_peer_id).await + } + + async fn send_msg_internal( + peers: &Arc, + foreign_network_client: &Arc, + msg: ZCPacket, + dst_peer_id: PeerId, + ) -> Result<(), Error> { + let policy = + Self::get_next_hop_policy(msg.peer_manager_header().unwrap().is_latency_first()); + + if let Some(gateway) = peers.get_gateway_peer_id(dst_peer_id, policy.clone()).await { + if peers.has_peer(gateway) { + peers.send_msg_directly(msg, gateway).await + } else if foreign_network_client.has_next_hop(gateway) { + foreign_network_client.send_msg(msg, gateway).await + } else { + tracing::warn!( + ?gateway, + ?dst_peer_id, + "cannot send msg to peer through gateway" + ); + Err(Error::RouteError(None)) + } + } else if foreign_network_client.has_next_hop(dst_peer_id) { + // check foreign network again. so in happy path we can avoid extra check + foreign_network_client.send_msg(msg, dst_peer_id).await + } else { + tracing::debug!(?dst_peer_id, "no gateway for peer"); + Err(Error::RouteError(None)) + } + } + + pub async fn get_msg_dst_peer(&self, ipv4_addr: &Ipv4Addr) -> (Vec, bool) { + let mut is_exit_node = false; + let mut dst_peers = vec![]; + let network_length = self + .global_ctx + .get_ipv4() + .map(|x| x.network_length()) + .unwrap_or(24); + let ipv4_inet = cidr::Ipv4Inet::new(*ipv4_addr, network_length).unwrap(); + if ipv4_addr.is_broadcast() + || ipv4_addr.is_multicast() + || *ipv4_addr == ipv4_inet.last_address() + { + dst_peers.extend( + self.peers + .list_routes() + .await + .iter() + .map(|x| x.key().clone()), + ); + } else if let Some(peer_id) = self.peers.get_peer_id_by_ipv4(&ipv4_addr).await { + dst_peers.push(peer_id); + } else { + for exit_node in &self.exit_nodes { + if let Some(peer_id) = self.peers.get_peer_id_by_ipv4(exit_node).await { + dst_peers.push(peer_id); + is_exit_node = true; + break; + } + } + } + + (dst_peers, is_exit_node) + } + + pub async fn try_compress_and_encrypt( + compress_algo: CompressorAlgo, + encryptor: &Box, + msg: &mut ZCPacket, + ) -> Result<(), Error> { + let compressor = DefaultCompressor {}; + compressor + .compress(msg, compress_algo) + .await + .with_context(|| "compress failed")?; + encryptor.encrypt(msg).with_context(|| "encrypt failed")?; + Ok(()) + } + + pub async fn send_msg_ipv4(&self, mut msg: ZCPacket, ipv4_addr: Ipv4Addr) -> Result<(), Error> { + tracing::trace!( + "do send_msg in peer manager, msg: {:?}, ipv4_addr: {}", + msg, + ipv4_addr + ); + + msg.fill_peer_manager_hdr( + self.my_peer_id, + 0, + tunnel::packet_def::PacketType::Data as u8, + ); + self.run_nic_packet_process_pipeline(&mut msg).await; + let cur_to_peer_id = msg.peer_manager_header().unwrap().to_peer_id.into(); + if cur_to_peer_id != 0 { + return Self::send_msg_internal( + &self.peers, + &self.foreign_network_client, + msg, + cur_to_peer_id, + ) + .await; + } + + let (dst_peers, is_exit_node) = self.get_msg_dst_peer(&ipv4_addr).await; + + if dst_peers.is_empty() { + tracing::info!("no peer id for ipv4: {}", ipv4_addr); + return Ok(()); + } + + Self::try_compress_and_encrypt(self.data_compress_algo, &self.encryptor, &mut msg).await?; + + let is_latency_first = self.global_ctx.get_flags().latency_first; + msg.mut_peer_manager_header() + .unwrap() + .set_latency_first(is_latency_first) + .set_exit_node(is_exit_node); + + let mut errs: Vec = vec![]; + let mut msg = Some(msg); + let total_dst_peers = dst_peers.len(); + for i in 0..total_dst_peers { + let mut msg = if i == total_dst_peers - 1 { + msg.take().unwrap() + } else { + msg.clone().unwrap() + }; + + let peer_id = &dst_peers[i]; + msg.mut_peer_manager_header() + .unwrap() + .to_peer_id + .set(*peer_id); + + if let Err(e) = + Self::send_msg_internal(&self.peers, &self.foreign_network_client, msg, *peer_id) + .await + { + errs.push(e); + } + } + + tracing::trace!(?dst_peers, "do send_msg in peer manager done"); + + if errs.is_empty() { + Ok(()) + } else { + tracing::error!(?errs, "send_msg has error"); + Err(anyhow::anyhow!("send_msg has error: {:?}", errs).into()) + } + } + + async fn run_clean_peer_without_conn_routine(&self) { + let peer_map = self.peers.clone(); + self.tasks.lock().await.spawn(async move { + loop { + peer_map.clean_peer_without_conn().await; + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + } + }); + } + + async fn run_foriegn_network(&self) { + self.peer_rpc_tspt + .foreign_peers + .lock() + .await + .replace(Arc::downgrade(&self.foreign_network_client)); + + self.foreign_network_client.run().await; + } + + pub async fn run(&self) -> Result<(), Error> { + match &self.route_algo_inst { + RouteAlgoInst::Ospf(route) => self.add_route(route.clone()).await, + RouteAlgoInst::None => {} + }; + + self.init_packet_process_pipeline().await; + self.peer_rpc_mgr.run(); + + self.start_peer_recv().await; + self.run_clean_peer_without_conn_routine().await; + + self.run_foriegn_network().await; + + Ok(()) + } + + pub fn get_peer_map(&self) -> Arc { + self.peers.clone() + } + + pub fn get_peer_rpc_mgr(&self) -> Arc { + self.peer_rpc_mgr.clone() + } + + pub fn my_node_id(&self) -> uuid::Uuid { + self.global_ctx.get_id() + } + + pub fn my_peer_id(&self) -> PeerId { + self.my_peer_id + } + + pub fn get_global_ctx(&self) -> ArcGlobalCtx { + self.global_ctx.clone() + } + + pub fn get_nic_channel(&self) -> PacketRecvChan { + self.nic_channel.clone() + } + + pub fn get_foreign_network_manager(&self) -> Arc { + self.foreign_network_manager.clone() + } + + pub fn get_foreign_network_client(&self) -> Arc { + self.foreign_network_client.clone() + } + + pub fn get_my_info(&self) -> cli::NodeInfo { + cli::NodeInfo { + peer_id: self.my_peer_id, + ipv4_addr: self + .global_ctx + .get_ipv4() + .map(|x| x.to_string()) + .unwrap_or_default(), + proxy_cidrs: self + .global_ctx + .get_proxy_cidrs() + .into_iter() + .map(|x| x.to_string()) + .collect(), + hostname: self.global_ctx.get_hostname(), + stun_info: Some(self.global_ctx.get_stun_info_collector().get_stun_info()), + inst_id: self.global_ctx.get_id().to_string(), + listeners: self + .global_ctx + .get_running_listeners() + .iter() + .map(|x| x.to_string()) + .collect(), + config: self.global_ctx.config.dump(), + version: EASYTIER_VERSION.to_string(), + feature_flag: Some(self.global_ctx.get_feature_flags()), + } + } + + pub async fn wait(&self) { + while !self.tasks.lock().await.is_empty() { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + } +} + +#[cfg(test)] +mod tests { + + use std::{fmt::Debug, sync::Arc, time::Duration}; + + use crate::{ + common::{config::Flags, global_ctx::tests::get_mock_global_ctx}, + connector::{ + create_connector_by_url, udp_hole_punch::tests::create_mock_peer_manager_with_mock_stun, + }, + instance::listeners::get_listener_by_url, + peers::{ + create_packet_recv_chan, + peer_manager::RouteAlgoType, + peer_rpc::tests::register_service, + route_trait::NextHopPolicy, + tests::{connect_peer_manager, wait_route_appear, wait_route_appear_with_cost}, + }, + proto::common::{CompressionAlgoPb, NatType, PeerFeatureFlag}, + tunnel::{common::tests::wait_for_condition, TunnelConnector, TunnelListener}, + }; + + use super::PeerManager; + + #[tokio::test] + async fn drop_peer_manager() { + let peer_mgr_a = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let peer_mgr_b = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let peer_mgr_c = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + connect_peer_manager(peer_mgr_a.clone(), peer_mgr_b.clone()).await; + connect_peer_manager(peer_mgr_b.clone(), peer_mgr_c.clone()).await; + connect_peer_manager(peer_mgr_a.clone(), peer_mgr_c.clone()).await; + + wait_route_appear(peer_mgr_a.clone(), peer_mgr_b.clone()) + .await + .unwrap(); + wait_route_appear(peer_mgr_a.clone(), peer_mgr_c.clone()) + .await + .unwrap(); + + // wait mgr_a have 2 peers + wait_for_condition( + || async { peer_mgr_a.get_peer_map().list_peers_with_conn().await.len() == 2 }, + std::time::Duration::from_secs(5), + ) + .await; + + drop(peer_mgr_b); + + wait_for_condition( + || async { peer_mgr_a.get_peer_map().list_peers_with_conn().await.len() == 1 }, + std::time::Duration::from_secs(5), + ) + .await; + } + + async fn connect_peer_manager_with( + client_mgr: Arc, + server_mgr: &Arc, + mut client: C, + server: &mut L, + ) { + server.listen().await.unwrap(); + + tokio::spawn(async move { + client.set_bind_addrs(vec![]); + client_mgr.try_connect(client).await.unwrap(); + }); + + server_mgr + .add_client_tunnel(server.accept().await.unwrap()) + .await + .unwrap(); + } + + #[rstest::rstest] + #[tokio::test] + #[serial_test::serial(forward_packet_test)] + async fn forward_packet( + #[values("tcp", "udp", "wg", "quic")] proto1: &str, + #[values("tcp", "udp", "wg", "quic")] proto2: &str, + ) { + use crate::proto::{ + rpc_impl::RpcController, + tests::{GreetingClientFactory, SayHelloRequest}, + }; + + let peer_mgr_a = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + register_service(&peer_mgr_a.peer_rpc_mgr, "", 0, "hello a"); + + let peer_mgr_b = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + + let peer_mgr_c = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + register_service(&peer_mgr_c.peer_rpc_mgr, "", 0, "hello c"); + + let mut listener1 = get_listener_by_url( + &format!("{}://0.0.0.0:31013", proto1).parse().unwrap(), + peer_mgr_b.get_global_ctx(), + ) + .unwrap(); + let connector1 = create_connector_by_url( + format!("{}://127.0.0.1:31013", proto1).as_str(), + &peer_mgr_a.get_global_ctx(), + ) + .await + .unwrap(); + connect_peer_manager_with(peer_mgr_a.clone(), &peer_mgr_b, connector1, &mut listener1) + .await; + + wait_route_appear(peer_mgr_a.clone(), peer_mgr_b.clone()) + .await + .unwrap(); + + let mut listener2 = get_listener_by_url( + &format!("{}://0.0.0.0:31014", proto2).parse().unwrap(), + peer_mgr_c.get_global_ctx(), + ) + .unwrap(); + let connector2 = create_connector_by_url( + format!("{}://127.0.0.1:31014", proto2).as_str(), + &peer_mgr_b.get_global_ctx(), + ) + .await + .unwrap(); + connect_peer_manager_with(peer_mgr_b.clone(), &peer_mgr_c, connector2, &mut listener2) + .await; + + wait_route_appear(peer_mgr_a.clone(), peer_mgr_c.clone()) + .await + .unwrap(); + + let stub = peer_mgr_a + .peer_rpc_mgr + .rpc_client() + .scoped_client::>( + peer_mgr_a.my_peer_id, + peer_mgr_c.my_peer_id, + "".to_string(), + ); + + let ret = stub + .say_hello( + RpcController::default(), + SayHelloRequest { + name: "abc".to_string(), + }, + ) + .await + .unwrap(); + + assert_eq!(ret.greeting, "hello c abc!"); + } + + #[tokio::test] + async fn communicate_between_enc_and_non_enc() { + let create_mgr = |enable_encryption| async move { + let (s, _r) = create_packet_recv_chan(); + let mock_global_ctx = get_mock_global_ctx(); + mock_global_ctx.config.set_flags(Flags { + enable_encryption, + data_compress_algo: CompressionAlgoPb::Zstd.into(), + ..Default::default() + }); + let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, mock_global_ctx, s)); + peer_mgr.run().await.unwrap(); + peer_mgr + }; + + let peer_mgr_a = create_mgr(true).await; + let peer_mgr_b = create_mgr(false).await; + + connect_peer_manager(peer_mgr_a.clone(), peer_mgr_b.clone()).await; + + // wait 5sec should not crash. + tokio::time::sleep(Duration::from_secs(5)).await; + + // both mgr should alive + let mgr_c = create_mgr(true).await; + connect_peer_manager(peer_mgr_a.clone(), mgr_c.clone()).await; + wait_route_appear(mgr_c, peer_mgr_a).await.unwrap(); + + let mgr_d = create_mgr(false).await; + connect_peer_manager(peer_mgr_b.clone(), mgr_d.clone()).await; + wait_route_appear(mgr_d, peer_mgr_b).await.unwrap(); + } + + #[tokio::test] + async fn test_avoid_relay_data() { + // a->b->c + // a->d->e->c + let peer_mgr_a = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let peer_mgr_b = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let peer_mgr_c = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let peer_mgr_d = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let peer_mgr_e = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + + connect_peer_manager(peer_mgr_a.clone(), peer_mgr_b.clone()).await; + connect_peer_manager(peer_mgr_b.clone(), peer_mgr_c.clone()).await; + + connect_peer_manager(peer_mgr_a.clone(), peer_mgr_d.clone()).await; + connect_peer_manager(peer_mgr_d.clone(), peer_mgr_e.clone()).await; + connect_peer_manager(peer_mgr_e.clone(), peer_mgr_c.clone()).await; + + // when b's avoid_relay_data is false, a->c should route through b and cost is 2 + wait_route_appear_with_cost(peer_mgr_a.clone(), peer_mgr_c.my_peer_id, Some(2)) + .await + .unwrap(); + let ret = peer_mgr_a + .get_route() + .get_next_hop_with_policy(peer_mgr_c.my_peer_id, NextHopPolicy::LeastCost) + .await; + assert_eq!(ret, Some(peer_mgr_b.my_peer_id)); + + // when b's avoid_relay_data is true, a->c should route through d and e, cost is 3 + peer_mgr_b + .get_global_ctx() + .set_feature_flags(PeerFeatureFlag { + avoid_relay_data: true, + ..Default::default() + }); + tokio::time::sleep(Duration::from_secs(2)).await; + wait_route_appear_with_cost(peer_mgr_a.clone(), peer_mgr_c.my_peer_id, Some(3)) + .await + .expect( + format!( + "route not appear, a route table: {}, table: {:#?}", + peer_mgr_a.get_route().dump().await, + peer_mgr_a.get_route().list_routes().await + ) + .as_str(), + ); + + let ret = peer_mgr_a + .get_route() + .get_next_hop_with_policy(peer_mgr_c.my_peer_id, NextHopPolicy::LeastCost) + .await; + assert_eq!(ret, Some(peer_mgr_d.my_peer_id)); + + println!("route table: {:#?}", peer_mgr_a.list_routes().await); + + // drop e, path should go back to through b + drop(peer_mgr_e); + wait_route_appear_with_cost(peer_mgr_a.clone(), peer_mgr_c.my_peer_id, Some(2)) + .await + .unwrap(); + let ret = peer_mgr_a + .get_route() + .get_next_hop_with_policy(peer_mgr_c.my_peer_id, NextHopPolicy::LeastCost) + .await; + assert_eq!(ret, Some(peer_mgr_b.my_peer_id)); + } +} diff --git a/rust/easytier/src/peers/peer_map.rs b/rust/easytier/src/peers/peer_map.rs new file mode 100644 index 0000000..26aaf37 --- /dev/null +++ b/rust/easytier/src/peers/peer_map.rs @@ -0,0 +1,292 @@ +use std::{net::Ipv4Addr, sync::Arc}; + +use anyhow::Context; +use dashmap::DashMap; +use tokio::sync::RwLock; + +use crate::{ + common::{ + error::Error, + global_ctx::{ArcGlobalCtx, GlobalCtxEvent, NetworkIdentity}, + PeerId, + }, + proto::{cli::PeerConnInfo, common::PeerFeatureFlag}, + tunnel::{packet_def::ZCPacket, TunnelError}, +}; + +use super::{ + peer::Peer, + peer_conn::{PeerConn, PeerConnId}, + route_trait::{ArcRoute, NextHopPolicy}, + PacketRecvChan, +}; + +pub struct PeerMap { + global_ctx: ArcGlobalCtx, + my_peer_id: PeerId, + peer_map: DashMap>, + packet_send: PacketRecvChan, + routes: RwLock>, +} + +impl PeerMap { + pub fn new(packet_send: PacketRecvChan, global_ctx: ArcGlobalCtx, my_peer_id: PeerId) -> Self { + PeerMap { + global_ctx, + my_peer_id, + peer_map: DashMap::new(), + packet_send, + routes: RwLock::new(Vec::new()), + } + } + + async fn add_new_peer(&self, peer: Peer) { + let peer_id = peer.peer_node_id.clone(); + self.peer_map.insert(peer_id.clone(), Arc::new(peer)); + self.global_ctx + .issue_event(GlobalCtxEvent::PeerAdded(peer_id)); + } + + pub async fn add_new_peer_conn(&self, peer_conn: PeerConn) { + let peer_id = peer_conn.get_peer_id(); + let no_entry = self.peer_map.get(&peer_id).is_none(); + if no_entry { + let new_peer = Peer::new(peer_id, self.packet_send.clone(), self.global_ctx.clone()); + new_peer.add_peer_conn(peer_conn).await; + self.add_new_peer(new_peer).await; + } else { + let peer = self.peer_map.get(&peer_id).unwrap().clone(); + peer.add_peer_conn(peer_conn).await; + } + } + + fn get_peer_by_id(&self, peer_id: PeerId) -> Option> { + self.peer_map.get(&peer_id).map(|v| v.clone()) + } + + pub fn has_peer(&self, peer_id: PeerId) -> bool { + peer_id == self.my_peer_id || self.peer_map.contains_key(&peer_id) + } + + pub async fn send_msg_directly(&self, msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error> { + if dst_peer_id == self.my_peer_id { + let packet_send = self.packet_send.clone(); + tokio::spawn(async move { + let ret = packet_send + .send(msg) + .await + .with_context(|| "send msg to self failed"); + if ret.is_err() { + tracing::error!("send msg to self failed: {:?}", ret); + } + }); + return Ok(()); + } + + match self.get_peer_by_id(dst_peer_id) { + Some(peer) => { + peer.send_msg(msg).await?; + } + None => { + tracing::error!("no peer for dst_peer_id: {}", dst_peer_id); + return Err(Error::RouteError(Some(format!( + "peer map sengmsg directly no connected dst_peer_id: {}", + dst_peer_id + )))); + } + } + + Ok(()) + } + + pub async fn get_gateway_peer_id( + &self, + dst_peer_id: PeerId, + policy: NextHopPolicy, + ) -> Option { + if dst_peer_id == self.my_peer_id { + return Some(dst_peer_id); + } + + if self.has_peer(dst_peer_id) && matches!(policy, NextHopPolicy::LeastHop) { + return Some(dst_peer_id); + } + + // get route info + for route in self.routes.read().await.iter() { + if let Some(gateway_peer_id) = route + .get_next_hop_with_policy(dst_peer_id, policy.clone()) + .await + { + // NOTIC: for foreign network, gateway_peer_id may not connect to me + return Some(gateway_peer_id); + } + } + + None + } + + pub async fn list_peers_own_foreign_network( + &self, + network_identity: &NetworkIdentity, + ) -> Vec { + let mut ret = Vec::new(); + for route in self.routes.read().await.iter() { + let peers = route + .list_peers_own_foreign_network(&network_identity) + .await; + ret.extend(peers); + } + ret + } + + pub async fn send_msg( + &self, + msg: ZCPacket, + dst_peer_id: PeerId, + policy: NextHopPolicy, + ) -> Result<(), Error> { + let Some(gateway_peer_id) = self.get_gateway_peer_id(dst_peer_id, policy).await else { + return Err(Error::RouteError(Some(format!( + "peer map sengmsg no gateway for dst_peer_id: {}", + dst_peer_id + )))); + }; + + self.send_msg_directly(msg, gateway_peer_id).await?; + return Ok(()); + } + + pub async fn get_peer_id_by_ipv4(&self, ipv4: &Ipv4Addr) -> Option { + for route in self.routes.read().await.iter() { + let peer_id = route.get_peer_id_by_ipv4(ipv4).await; + if peer_id.is_some() { + return peer_id; + } + } + None + } + + pub async fn get_peer_feature_flag(&self, peer_id: PeerId) -> Option { + for route in self.routes.read().await.iter() { + let feature_flag = route.get_feature_flag(peer_id).await; + if feature_flag.is_some() { + return feature_flag; + }; + } + None + } + + pub fn is_empty(&self) -> bool { + self.peer_map.is_empty() + } + + pub async fn list_peers(&self) -> Vec { + let mut ret = Vec::new(); + for item in self.peer_map.iter() { + let peer_id = item.key(); + ret.push(*peer_id); + } + ret + } + + pub async fn list_peers_with_conn(&self) -> Vec { + let mut ret = Vec::new(); + let peers = self.list_peers().await; + for peer_id in peers.iter() { + let Some(peer) = self.get_peer_by_id(*peer_id) else { + continue; + }; + if peer.list_peer_conns().await.len() > 0 { + ret.push(*peer_id); + } + } + ret + } + + pub async fn list_peer_conns(&self, peer_id: PeerId) -> Option> { + if let Some(p) = self.get_peer_by_id(peer_id) { + Some(p.list_peer_conns().await) + } else { + return None; + } + } + + pub async fn close_peer_conn( + &self, + peer_id: PeerId, + conn_id: &PeerConnId, + ) -> Result<(), Error> { + if let Some(p) = self.get_peer_by_id(peer_id) { + p.close_peer_conn(conn_id).await + } else { + return Err(Error::NotFound); + } + } + + pub async fn close_peer(&self, peer_id: PeerId) -> Result<(), TunnelError> { + let remove_ret = self.peer_map.remove(&peer_id); + self.global_ctx + .issue_event(GlobalCtxEvent::PeerRemoved(peer_id)); + tracing::info!( + ?peer_id, + has_old_value = ?remove_ret.is_some(), + peer_ref_counter = ?remove_ret.map(|v| Arc::strong_count(&v.1)), + "peer is closed" + ); + Ok(()) + } + + pub async fn add_route(&self, route: ArcRoute) { + let mut routes = self.routes.write().await; + routes.insert(0, route); + } + + pub async fn clean_peer_without_conn(&self) { + let mut to_remove = vec![]; + + for peer_id in self.list_peers().await { + let conns = self.list_peer_conns(peer_id).await; + if conns.is_none() || conns.as_ref().unwrap().is_empty() { + to_remove.push(peer_id); + } + } + + for peer_id in to_remove { + self.close_peer(peer_id).await.unwrap(); + } + } + + pub async fn list_routes(&self) -> DashMap { + let route_map = DashMap::new(); + for route in self.routes.read().await.iter() { + for item in route.list_routes().await.iter() { + route_map.insert(item.peer_id, item.next_hop_peer_id); + } + } + route_map + } + + pub async fn need_relay_by_foreign_network(&self, dst_peer_id: PeerId) -> Result { + // if gateway_peer_id is not connected to me, means need relay by foreign network + let gateway_id = self + .get_gateway_peer_id(dst_peer_id, NextHopPolicy::LeastHop) + .await + .ok_or(Error::RouteError(Some(format!( + "peer map need_relay_by_foreign_network no gateway for dst_peer_id: {}", + dst_peer_id + ))))?; + + Ok(!self.has_peer(gateway_id)) + } +} + +impl Drop for PeerMap { + fn drop(&mut self) { + tracing::debug!( + self.my_peer_id, + network = ?self.global_ctx.get_network_identity(), + "PeerMap is dropped" + ); + } +} diff --git a/rust/easytier/src/peers/peer_ospf_route.rs b/rust/easytier/src/peers/peer_ospf_route.rs new file mode 100644 index 0000000..19a418c --- /dev/null +++ b/rust/easytier/src/peers/peer_ospf_route.rs @@ -0,0 +1,2675 @@ +use std::{ + collections::BTreeSet, + fmt::Debug, + net::Ipv4Addr, + sync::{ + atomic::{AtomicBool, AtomicU32, Ordering}, + Arc, Weak, + }, + time::{Duration, Instant, SystemTime}, +}; + +use crossbeam::atomic::AtomicCell; +use dashmap::DashMap; +use petgraph::{ + algo::{all_simple_paths, astar, dijkstra}, + graph::NodeIndex, + Directed, Graph, +}; +use prost::Message; +use prost_reflect::{DynamicMessage, ReflectMessage}; +use serde::{Deserialize, Serialize}; +use tokio::{ + select, + sync::Mutex, + task::{JoinHandle, JoinSet}, +}; + +use crate::{ + common::{ + config::NetworkIdentity, constants::EASYTIER_VERSION, global_ctx::ArcGlobalCtx, + stun::StunInfoCollectorTrait, PeerId, + }, + peers::route_trait::{Route, RouteInterfaceBox}, + proto::{ + common::{Ipv4Inet, NatType, PeerFeatureFlag, StunInfo}, + peer_rpc::{ + route_foreign_network_infos, ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, + OspfRouteRpc, OspfRouteRpcClientFactory, OspfRouteRpcServer, PeerIdVersion, + RouteForeignNetworkInfos, RoutePeerInfo, RoutePeerInfos, SyncRouteInfoError, + SyncRouteInfoRequest, SyncRouteInfoResponse, + }, + rpc_types::{ + self, + controller::{BaseController, Controller}, + }, + }, + use_global_var, +}; + +use super::{ + peer_rpc::PeerRpcManager, + route_trait::{ + DefaultRouteCostCalculator, ForeignNetworkRouteInfoMap, NextHopPolicy, RouteCostCalculator, + RouteCostCalculatorInterface, + }, + PeerPacketFilter, +}; + +static SERVICE_ID: u32 = 7; +static UPDATE_PEER_INFO_PERIOD: Duration = Duration::from_secs(3600); +static REMOVE_DEAD_PEER_INFO_AFTER: Duration = Duration::from_secs(3660); +static AVOID_RELAY_COST: i32 = i32::MAX / 512; + +type Version = u32; + +#[derive(Debug, Clone)] +struct AtomicVersion(Arc); + +impl AtomicVersion { + fn new() -> Self { + AtomicVersion(Arc::new(AtomicU32::new(0))) + } + + fn get(&self) -> Version { + self.0.load(Ordering::Relaxed) + } + + fn set(&self, version: Version) { + self.0.store(version, Ordering::Relaxed); + } + + fn inc(&self) { + self.0.fetch_add(1, Ordering::Relaxed); + } + + fn set_if_larger(&self, version: Version) { + if self.get() < version { + self.set(version); + } + } +} + +impl From for AtomicVersion { + fn from(version: Version) -> Self { + AtomicVersion(Arc::new(AtomicU32::new(version))) + } +} + +fn is_foreign_network_info_newer( + next: &ForeignNetworkRouteInfoEntry, + prev: &ForeignNetworkRouteInfoEntry, +) -> Option { + Some( + SystemTime::try_from(next.last_update?).ok()? + > SystemTime::try_from(prev.last_update?).ok()?, + ) +} + +impl RoutePeerInfo { + pub fn new() -> Self { + Self { + peer_id: 0, + inst_id: Some(uuid::Uuid::nil().into()), + cost: 0, + ipv4_addr: None, + proxy_cidrs: Vec::new(), + hostname: None, + udp_stun_info: 0, + last_update: Some(SystemTime::now().into()), + version: 0, + easytier_version: EASYTIER_VERSION.to_string(), + feature_flag: None, + peer_route_id: 0, + network_length: 24, + } + } + + pub fn update_self( + &self, + my_peer_id: PeerId, + peer_route_id: u64, + global_ctx: &ArcGlobalCtx, + ) -> Self { + let mut new = Self { + peer_id: my_peer_id, + inst_id: Some(global_ctx.get_id().into()), + cost: 0, + ipv4_addr: global_ctx.get_ipv4().map(|x| x.address().into()), + proxy_cidrs: global_ctx + .get_proxy_cidrs() + .iter() + .map(|x| x.to_string()) + .chain(global_ctx.get_vpn_portal_cidr().map(|x| x.to_string())) + .collect(), + hostname: Some(global_ctx.get_hostname()), + udp_stun_info: global_ctx + .get_stun_info_collector() + .get_stun_info() + .udp_nat_type, + // following fields do not participate in comparison. + last_update: self.last_update, + version: self.version, + + easytier_version: EASYTIER_VERSION.to_string(), + feature_flag: Some(global_ctx.get_feature_flags()), + peer_route_id, + network_length: global_ctx + .get_ipv4() + .map(|x| x.network_length() as u32) + .unwrap_or(24), + }; + + let need_update_periodically = if let Ok(Ok(d)) = + SystemTime::try_from(new.last_update.unwrap()).map(|x| x.elapsed()) + { + d > UPDATE_PEER_INFO_PERIOD + } else { + true + }; + + if new != *self || need_update_periodically { + new.last_update = Some(SystemTime::now().into()); + new.version += 1; + } + + new + } +} + +impl Into for RoutePeerInfo { + fn into(self) -> crate::proto::cli::Route { + let network_length = if self.network_length == 0 { + 24 + } else { + self.network_length + }; + + crate::proto::cli::Route { + peer_id: self.peer_id, + ipv4_addr: if let Some(ipv4_addr) = self.ipv4_addr { + Some(Ipv4Inet { + address: Some(ipv4_addr.into()), + network_length, + }) + } else { + None + }, + next_hop_peer_id: 0, // next_hop_peer_id is calculated in RouteTable. + cost: 0, // cost is calculated in RouteTable. + path_latency: 0, // path_latency is calculated in RouteTable. + proxy_cidrs: self.proxy_cidrs.clone(), + hostname: self.hostname.unwrap_or_default(), + stun_info: { + let mut stun_info = StunInfo::default(); + if let Ok(udp_nat_type) = NatType::try_from(self.udp_stun_info as i32) { + stun_info.set_udp_nat_type(udp_nat_type); + } + Some(stun_info) + }, + inst_id: self.inst_id.map(|x| x.to_string()).unwrap_or_default(), + version: self.easytier_version, + feature_flag: self.feature_flag, + + next_hop_peer_id_latency_first: None, + cost_latency_first: None, + path_latency_latency_first: None, + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +struct RouteConnBitmap { + peer_ids: Vec<(PeerId, Version)>, + bitmap: Vec, +} + +impl Into for RouteConnBitmap { + fn into(self) -> crate::proto::peer_rpc::RouteConnBitmap { + crate::proto::peer_rpc::RouteConnBitmap { + peer_ids: self + .peer_ids + .into_iter() + .map(|x| PeerIdVersion { + peer_id: x.0, + version: x.1, + }) + .collect(), + bitmap: self.bitmap, + } + } +} + +impl From for RouteConnBitmap { + fn from(v: crate::proto::peer_rpc::RouteConnBitmap) -> Self { + RouteConnBitmap { + peer_ids: v + .peer_ids + .into_iter() + .map(|x| (x.peer_id, x.version)) + .collect(), + bitmap: v.bitmap, + } + } +} + +impl RouteConnBitmap { + fn new() -> Self { + RouteConnBitmap { + peer_ids: Vec::new(), + bitmap: Vec::new(), + } + } + + fn get_bit(&self, idx: usize) -> bool { + let byte_idx = idx / 8; + let bit_idx = idx % 8; + let byte = self.bitmap[byte_idx]; + (byte >> bit_idx) & 1 == 1 + } + + fn get_connected_peers(&self, peer_idx: usize) -> BTreeSet { + let mut connected_peers = BTreeSet::new(); + for (idx, (peer_id, _)) in self.peer_ids.iter().enumerate() { + if self.get_bit(peer_idx * self.peer_ids.len() + idx) { + connected_peers.insert(*peer_id); + } + } + connected_peers + } +} + +type Error = SyncRouteInfoError; + +// constructed with all infos synced from all peers. +#[derive(Debug)] +struct SyncedRouteInfo { + peer_infos: DashMap, + // prost doesn't support unknown fields, so we use DynamicMessage to store raw infos and progate them to other peers. + raw_peer_infos: DashMap, + conn_map: DashMap, AtomicVersion)>, + foreign_network: DashMap, +} + +impl SyncedRouteInfo { + fn get_connected_peers>(&self, peer_id: PeerId) -> Option { + self.conn_map + .get(&peer_id) + .map(|x| x.0.clone().iter().map(|x| *x).collect()) + } + + fn remove_peer(&self, peer_id: PeerId) { + tracing::warn!(?peer_id, "remove_peer from synced_route_info"); + self.peer_infos.remove(&peer_id); + self.raw_peer_infos.remove(&peer_id); + self.conn_map.remove(&peer_id); + self.foreign_network.retain(|k, _| k.peer_id != peer_id); + } + + fn fill_empty_peer_info(&self, peer_ids: &BTreeSet) { + for peer_id in peer_ids { + self.peer_infos + .entry(*peer_id) + .or_insert_with(|| RoutePeerInfo::new()); + + self.conn_map + .entry(*peer_id) + .or_insert_with(|| (BTreeSet::new(), AtomicVersion::new())); + } + } + + fn get_peer_info_version_with_default(&self, peer_id: PeerId) -> Version { + self.peer_infos + .get(&peer_id) + .map(|x| x.version) + .unwrap_or(0) + } + + fn get_avoid_relay_data(&self, peer_id: PeerId) -> bool { + // if avoid relay, just set all outgoing edges to a large value: AVOID_RELAY_COST. + self.peer_infos + .get(&peer_id) + .and_then(|x| x.value().feature_flag) + .map(|x| x.avoid_relay_data) + .unwrap_or_default() + } + + fn check_duplicate_peer_id( + &self, + my_peer_id: PeerId, + my_peer_route_id: u64, + dst_peer_id: PeerId, + dst_peer_route_id: Option, + info: &RoutePeerInfo, + ) -> Result<(), Error> { + // 1. check if we are duplicated. + if info.peer_id == my_peer_id { + if info.peer_route_id != my_peer_route_id + && info.version > self.get_peer_info_version_with_default(info.peer_id) + { + // if dst peer send to us with higher version info of my peer, our peer id is duplicated + // TODO: handle this better. restart peer manager? + panic!("my peer id is duplicated"); + // return Err(Error::DuplicatePeerId); + } + } else if info.peer_id == dst_peer_id { + let Some(dst_peer_route_id) = dst_peer_route_id else { + return Ok(()); + }; + + if dst_peer_route_id != info.peer_route_id + && info.version < self.get_peer_info_version_with_default(info.peer_id) + { + // if dst peer send to us with lower version info of dst peer, dst peer id is duplicated + return Err(Error::DuplicatePeerId); + } + } + + Ok(()) + } + + fn update_peer_infos( + &self, + my_peer_id: PeerId, + my_peer_route_id: u64, + dst_peer_id: PeerId, + peer_infos: &Vec, + raw_peer_infos: &Vec, + ) -> Result<(), Error> { + for (idx, route_info) in peer_infos.iter().enumerate() { + let mut route_info = route_info.clone(); + let raw_route_info = &raw_peer_infos[idx]; + self.check_duplicate_peer_id( + my_peer_id, + my_peer_route_id, + dst_peer_id, + if route_info.peer_id == dst_peer_id { + self.peer_infos.get(&dst_peer_id).map(|x| x.peer_route_id) + } else { + None + }, + &route_info, + )?; + + let peer_id_raw = raw_route_info + .get_field_by_name("peer_id") + .unwrap() + .as_u32() + .unwrap(); + assert_eq!(peer_id_raw, route_info.peer_id); + + // time between peers may not be synchronized, so update last_update to local now. + // note only last_update with larger version will be updated to local saved peer info. + route_info.last_update = Some(SystemTime::now().into()); + + self.peer_infos + .entry(route_info.peer_id) + .and_modify(|old_entry| { + if route_info.version > old_entry.version { + self.raw_peer_infos + .insert(route_info.peer_id, raw_route_info.clone()); + *old_entry = route_info.clone(); + } + }) + .or_insert_with(|| { + self.raw_peer_infos + .insert(route_info.peer_id, raw_route_info.clone()); + route_info.clone() + }); + } + Ok(()) + } + + fn update_conn_map(&self, conn_bitmap: &RouteConnBitmap) { + self.fill_empty_peer_info(&conn_bitmap.peer_ids.iter().map(|x| x.0).collect()); + + for (peer_idx, (peer_id, version)) in conn_bitmap.peer_ids.iter().enumerate() { + assert!(self.peer_infos.contains_key(peer_id)); + let connceted_peers = conn_bitmap.get_connected_peers(peer_idx); + self.fill_empty_peer_info(&connceted_peers); + + self.conn_map + .entry(*peer_id) + .and_modify(|(old_conn_bitmap, old_version)| { + if *version > old_version.get() { + *old_conn_bitmap = conn_bitmap.get_connected_peers(peer_idx); + old_version.set(*version); + } + }) + .or_insert_with(|| { + ( + conn_bitmap.get_connected_peers(peer_idx), + version.clone().into(), + ) + }); + } + } + + fn update_foreign_network(&self, foreign_network: &RouteForeignNetworkInfos) { + for item in foreign_network.infos.iter().map(Clone::clone) { + let Some(key) = item.key else { + continue; + }; + let Some(mut entry) = item.value else { + continue; + }; + + entry.last_update = Some(SystemTime::now().into()); + + self.foreign_network + .entry(key.clone()) + .and_modify(|old_entry| { + if entry.version > old_entry.version { + *old_entry = entry.clone(); + } + }) + .or_insert_with(|| entry.clone()); + } + } + + fn update_my_peer_info( + &self, + my_peer_id: PeerId, + my_peer_route_id: u64, + global_ctx: &ArcGlobalCtx, + ) -> bool { + let mut old = self + .peer_infos + .entry(my_peer_id) + .or_insert(RoutePeerInfo::new()); + let new = old.update_self(my_peer_id, my_peer_route_id, &global_ctx); + let new_version = new.version; + let old_version = old.version; + *old = new; + + new_version != old_version + } + + fn update_my_conn_info(&self, my_peer_id: PeerId, connected_peers: BTreeSet) -> bool { + self.fill_empty_peer_info(&connected_peers); + + let mut my_conn_info = self + .conn_map + .entry(my_peer_id) + .or_insert((BTreeSet::new(), AtomicVersion::new())); + + if connected_peers == my_conn_info.value().0 { + false + } else { + let _ = std::mem::replace(&mut my_conn_info.value_mut().0, connected_peers); + my_conn_info.value().1.inc(); + true + } + } + + fn update_my_foreign_network( + &self, + my_peer_id: PeerId, + foreign_networks: ForeignNetworkRouteInfoMap, + ) -> bool { + let now = SystemTime::now(); + let now_version = now + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() as Version; + let mut updated = false; + for mut item in self + .foreign_network + .iter_mut() + .filter(|x| x.key().peer_id == my_peer_id) + { + let (key, entry) = item.pair_mut(); + if let Some(mut new_entry) = foreign_networks.get_mut(key) { + assert!(!new_entry.foreign_peer_ids.is_empty()); + if let Some(is_newer) = is_foreign_network_info_newer(&new_entry, entry) { + let need_renew = is_newer + || now + .duration_since(entry.last_update.unwrap().try_into().unwrap()) + .unwrap() + > UPDATE_PEER_INFO_PERIOD; + if need_renew { + new_entry.version = std::cmp::max(new_entry.version + 1, now_version); + *entry = new_entry.clone(); + updated = true; + } + } + drop(new_entry); + foreign_networks.remove(key).unwrap(); + } else if !item.foreign_peer_ids.is_empty() { + item.foreign_peer_ids.clear(); + item.last_update = Some(SystemTime::now().into()); + item.version = std::cmp::max(item.version + 1, now_version); + updated = true; + } + } + + for item in foreign_networks.iter() { + assert!(!item.value().foreign_peer_ids.is_empty()); + self.foreign_network + .entry(item.key().clone()) + .and_modify(|v| panic!("key should not exist, {:?}", v)) + .or_insert_with(|| { + let mut v = item.value().clone(); + v.version = now_version; + v + }); + updated = true; + } + + updated + } + + fn is_peer_bidirectly_connected(&self, src_peer_id: PeerId, dst_peer_id: PeerId) -> bool { + self.conn_map + .get(&src_peer_id) + .map(|x| x.0.contains(&dst_peer_id)) + .unwrap_or(false) + } + + fn is_peer_directly_connected(&self, src_peer_id: PeerId, dst_peer_id: PeerId) -> bool { + return self.is_peer_bidirectly_connected(src_peer_id, dst_peer_id) + || self.is_peer_bidirectly_connected(dst_peer_id, src_peer_id); + } +} + +type PeerGraph = Graph; +type PeerIdToNodexIdxMap = DashMap; +#[derive(Debug, Clone, Copy)] +struct NextHopInfo { + next_hop_peer_id: PeerId, + path_latency: i32, + path_len: usize, // path includes src and dst. +} +// dst_peer_id -> (next_hop_peer_id, cost, path_len) +type NextHopMap = DashMap; + +// computed with SyncedRouteInfo. used to get next hop. +#[derive(Debug)] +struct RouteTable { + peer_infos: DashMap, + next_hop_map: NextHopMap, + ipv4_peer_id_map: DashMap, + cidr_peer_id_map: DashMap, +} + +impl RouteTable { + fn new() -> Self { + RouteTable { + peer_infos: DashMap::new(), + next_hop_map: DashMap::new(), + ipv4_peer_id_map: DashMap::new(), + cidr_peer_id_map: DashMap::new(), + } + } + + fn get_next_hop(&self, dst_peer_id: PeerId) -> Option { + self.next_hop_map.get(&dst_peer_id).map(|x| *x) + } + + fn peer_reachable(&self, peer_id: PeerId) -> bool { + self.next_hop_map.contains_key(&peer_id) + } + + fn get_nat_type(&self, peer_id: PeerId) -> Option { + self.peer_infos + .get(&peer_id) + .map(|x| NatType::try_from(x.udp_stun_info as i32).unwrap_or_default()) + } + + fn build_peer_graph_from_synced_info( + peers: Vec, + synced_info: &SyncedRouteInfo, + cost_calc: &mut T, + ) -> (PeerGraph, PeerIdToNodexIdxMap) { + let mut graph: PeerGraph = Graph::new(); + let peer_id_to_node_index = PeerIdToNodexIdxMap::new(); + for peer_id in peers.iter() { + peer_id_to_node_index.insert(*peer_id, graph.add_node(*peer_id)); + } + + for peer_id in peers.iter() { + let connected_peers = synced_info + .get_connected_peers(*peer_id) + .unwrap_or(BTreeSet::new()); + + // if avoid relay, just set all outgoing edges to a large value: AVOID_RELAY_COST. + let peer_avoid_relay_data = synced_info.get_avoid_relay_data(*peer_id); + + for dst_peer_id in connected_peers.iter() { + let Some(dst_idx) = peer_id_to_node_index.get(dst_peer_id) else { + continue; + }; + + graph.add_edge( + *peer_id_to_node_index.get(&peer_id).unwrap(), + *dst_idx, + if peer_avoid_relay_data { + AVOID_RELAY_COST + } else { + cost_calc.calculate_cost(*peer_id, *dst_peer_id) + }, + ); + } + } + + (graph, peer_id_to_node_index) + } + + fn gen_next_hop_map_with_least_hop( + my_peer_id: PeerId, + graph: &PeerGraph, + idx_map: &PeerIdToNodexIdxMap, + cost_calc: &mut T, + ) -> NextHopMap { + let res = dijkstra(&graph, *idx_map.get(&my_peer_id).unwrap(), None, |_| 1); + let next_hop_map = NextHopMap::new(); + for (node_idx, cost) in res.iter() { + if *cost == 0 { + continue; + } + let mut all_paths = all_simple_paths::, _>( + graph, + *idx_map.get(&my_peer_id).unwrap(), + *node_idx, + *cost - 1, + Some(*cost + 1), // considering having avoid relay, the max cost could be a bit larger. + ) + .collect::>(); + + assert!(!all_paths.is_empty()); + all_paths.sort_by(|a, b| a.len().cmp(&b.len())); + + // find a path with least cost. + let mut min_cost = i32::MAX; + let mut min_path_len = usize::MAX; + let mut min_path = Vec::new(); + for path in all_paths.iter() { + if min_path_len < path.len() && min_cost < AVOID_RELAY_COST { + // the min path does not contain avoid relay node. + break; + } + + let mut cost = 0; + for i in 0..path.len() - 1 { + let src_peer_id = *graph.node_weight(path[i]).unwrap(); + let dst_peer_id = *graph.node_weight(path[i + 1]).unwrap(); + let edge_weight = *graph + .edge_weight(graph.find_edge(path[i], path[i + 1]).unwrap()) + .unwrap(); + if edge_weight != 1 { + // means avoid relay. + cost += edge_weight; + } else { + cost += cost_calc.calculate_cost(src_peer_id, dst_peer_id); + } + } + + if cost <= min_cost { + min_cost = cost; + min_path = path.clone(); + min_path_len = path.len(); + } + } + next_hop_map.insert( + *graph.node_weight(*node_idx).unwrap(), + NextHopInfo { + next_hop_peer_id: *graph.node_weight(min_path[1]).unwrap(), + path_latency: min_cost, + path_len: min_path_len, + }, + ); + } + + next_hop_map + } + + fn gen_next_hop_map_with_least_cost( + my_peer_id: PeerId, + graph: &PeerGraph, + idx_map: &PeerIdToNodexIdxMap, + ) -> NextHopMap { + let next_hop_map = NextHopMap::new(); + for item in idx_map.iter() { + if *item.key() == my_peer_id { + continue; + } + + let dst_peer_node_idx = *item.value(); + + let Some((cost, path)) = astar::astar( + graph, + *idx_map.get(&my_peer_id).unwrap(), + |node_idx| node_idx == dst_peer_node_idx, + |e| *e.weight(), + |_| 0, + ) else { + continue; + }; + + next_hop_map.insert( + *item.key(), + NextHopInfo { + next_hop_peer_id: *graph.node_weight(path[1]).unwrap(), + path_latency: cost, + path_len: path.len(), + }, + ); + } + + next_hop_map + } + + fn build_from_synced_info( + &self, + my_peer_id: PeerId, + synced_info: &SyncedRouteInfo, + policy: NextHopPolicy, + mut cost_calc: T, + ) { + // build peer_infos + self.peer_infos.clear(); + for item in synced_info.peer_infos.iter() { + let peer_id = item.key(); + let info = item.value(); + + if info.version == 0 { + continue; + } + + self.peer_infos.insert(*peer_id, info.clone()); + } + + if self.peer_infos.is_empty() { + return; + } + + // build next hop map + self.next_hop_map.clear(); + self.next_hop_map.insert( + my_peer_id, + NextHopInfo { + next_hop_peer_id: my_peer_id, + path_latency: 0, + path_len: 1, + }, + ); + let (graph, idx_map) = Self::build_peer_graph_from_synced_info( + self.peer_infos.iter().map(|x| *x.key()).collect(), + &synced_info, + &mut cost_calc, + ); + let next_hop_map = if matches!(policy, NextHopPolicy::LeastHop) { + Self::gen_next_hop_map_with_least_hop(my_peer_id, &graph, &idx_map, &mut cost_calc) + } else { + Self::gen_next_hop_map_with_least_cost(my_peer_id, &graph, &idx_map) + }; + for item in next_hop_map.iter() { + self.next_hop_map.insert(*item.key(), *item.value()); + } + // build graph + + // build ipv4_peer_id_map, cidr_peer_id_map + self.ipv4_peer_id_map.clear(); + self.cidr_peer_id_map.clear(); + for item in self.peer_infos.iter() { + // only set ipv4 map for peers we can reach. + if !self.next_hop_map.contains_key(item.key()) { + continue; + } + + let peer_id = item.key(); + let info = item.value(); + + if let Some(ipv4_addr) = info.ipv4_addr { + self.ipv4_peer_id_map.insert(ipv4_addr.into(), *peer_id); + } + + for cidr in info.proxy_cidrs.iter() { + self.cidr_peer_id_map + .insert(cidr.parse().unwrap(), *peer_id); + } + } + } + + fn get_peer_id_for_proxy(&self, ipv4: &Ipv4Addr) -> Option { + let ipv4 = std::net::IpAddr::V4(*ipv4); + for item in self.cidr_peer_id_map.iter() { + let (k, v) = item.pair(); + if k.contains(&ipv4) { + return Some(*v); + } + } + None + } +} + +type SessionId = u64; + +type AtomicSessionId = atomic_shim::AtomicU64; + +struct SessionTask { + my_peer_id: PeerId, + task: Arc>>>, +} + +impl SessionTask { + fn new(my_peer_id: PeerId) -> Self { + SessionTask { + my_peer_id, + task: Arc::new(std::sync::Mutex::new(None)), + } + } + + fn set_task(&self, task: JoinHandle<()>) { + if let Some(old) = self.task.lock().unwrap().replace(task) { + old.abort(); + } + } + + fn is_running(&self) -> bool { + if let Some(task) = self.task.lock().unwrap().as_ref() { + !task.is_finished() + } else { + false + } + } +} + +impl Drop for SessionTask { + fn drop(&mut self) { + if let Some(task) = self.task.lock().unwrap().take() { + task.abort(); + } + tracing::debug!(my_peer_id = self.my_peer_id, "drop SessionTask"); + } +} + +impl Debug for SessionTask { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SessionTask") + .field("is_running", &self.is_running()) + .finish() + } +} + +// if we need to sync route info with one peer, we create a SyncRouteSession with that peer. +#[derive(Debug)] +struct SyncRouteSession { + my_peer_id: PeerId, + dst_peer_id: PeerId, + dst_saved_peer_info_versions: DashMap, + dst_saved_conn_bitmap_version: DashMap, + dst_saved_foreign_network_versions: DashMap, + + my_session_id: AtomicSessionId, + dst_session_id: AtomicSessionId, + + // every node should have exactly one initator session to one other non-initiator peer. + we_are_initiator: AtomicBool, + dst_is_initiator: AtomicBool, + + need_sync_initiator_info: AtomicBool, + + rpc_tx_count: AtomicU32, + rpc_rx_count: AtomicU32, + + task: SessionTask, +} + +impl SyncRouteSession { + fn new(my_peer_id: PeerId, dst_peer_id: PeerId) -> Self { + SyncRouteSession { + my_peer_id, + dst_peer_id, + dst_saved_peer_info_versions: DashMap::new(), + dst_saved_conn_bitmap_version: DashMap::new(), + dst_saved_foreign_network_versions: DashMap::new(), + + my_session_id: AtomicSessionId::new(rand::random()), + dst_session_id: AtomicSessionId::new(0), + + we_are_initiator: AtomicBool::new(false), + dst_is_initiator: AtomicBool::new(false), + + need_sync_initiator_info: AtomicBool::new(false), + + rpc_tx_count: AtomicU32::new(0), + rpc_rx_count: AtomicU32::new(0), + + task: SessionTask::new(my_peer_id), + } + } + + fn check_saved_peer_info_update_to_date(&self, peer_id: PeerId, version: Version) -> bool { + if version == 0 || peer_id == self.dst_peer_id { + // never send version 0 peer info to dst peer. + return true; + } + self.dst_saved_peer_info_versions + .get(&peer_id) + .map(|v| v.get() >= version) + .unwrap_or(false) + } + + fn update_dst_saved_peer_info_version(&self, infos: &Vec) { + for info in infos.iter() { + self.dst_saved_peer_info_versions + .entry(info.peer_id) + .or_insert_with(|| AtomicVersion::new()) + .set_if_larger(info.version); + } + } + + fn update_dst_saved_conn_bitmap_version(&self, conn_bitmap: &RouteConnBitmap) { + for (peer_id, version) in conn_bitmap.peer_ids.iter() { + self.dst_saved_conn_bitmap_version + .entry(*peer_id) + .or_insert_with(|| AtomicVersion::new()) + .set_if_larger(*version); + } + } + + fn update_dst_saved_foreign_network_version(&self, foreign_network: &RouteForeignNetworkInfos) { + for item in foreign_network.infos.iter() { + self.dst_saved_foreign_network_versions + .entry(item.key.clone().unwrap()) + .or_insert_with(|| AtomicVersion::new()) + .set_if_larger(item.value.as_ref().unwrap().version); + } + } + + fn update_initiator_flag(&self, is_initiator: bool) { + self.we_are_initiator.store(is_initiator, Ordering::Relaxed); + self.need_sync_initiator_info.store(true, Ordering::Relaxed); + } + + fn update_dst_session_id(&self, session_id: SessionId) { + if session_id != self.dst_session_id.load(Ordering::Relaxed) { + tracing::warn!(?self, ?session_id, "session id mismatch, clear saved info."); + self.dst_session_id.store(session_id, Ordering::Relaxed); + self.dst_saved_conn_bitmap_version.clear(); + self.dst_saved_peer_info_versions.clear(); + } + } + + fn short_debug_string(&self) -> String { + format!( + "session_dst_peer: {:?}, my_session_id: {:?}, dst_session_id: {:?}, we_are_initiator: {:?}, dst_is_initiator: {:?}, rpc_tx_count: {:?}, rpc_rx_count: {:?}, task: {:?}", + self.dst_peer_id, + self.my_session_id, + self.dst_session_id, + self.we_are_initiator, + self.dst_is_initiator, + self.rpc_tx_count, + self.rpc_rx_count, + self.task + ) + } +} + +impl Drop for SyncRouteSession { + fn drop(&mut self) { + tracing::debug!(?self, "drop SyncRouteSession"); + } +} + +struct PeerRouteServiceImpl { + my_peer_id: PeerId, + my_peer_route_id: u64, + global_ctx: ArcGlobalCtx, + sessions: DashMap>, + + interface: Mutex>, + + cost_calculator: std::sync::Mutex>, + route_table: RouteTable, + route_table_with_cost: RouteTable, + foreign_network_owner_map: DashMap>, + synced_route_info: SyncedRouteInfo, + cached_local_conn_map: std::sync::Mutex, + + last_update_my_foreign_network: AtomicCell>, +} + +impl Debug for PeerRouteServiceImpl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PeerRouteServiceImpl") + .field("my_peer_id", &self.my_peer_id) + .field("my_peer_route_id", &self.my_peer_route_id) + .field("network", &self.global_ctx.get_network_identity()) + .field("sessions", &self.sessions) + .field("route_table", &self.route_table) + .field("route_table_with_cost", &self.route_table_with_cost) + .field("synced_route_info", &self.synced_route_info) + .field("foreign_network_owner_map", &self.foreign_network_owner_map) + .field( + "cached_local_conn_map", + &self.cached_local_conn_map.lock().unwrap(), + ) + .finish() + } +} + +impl PeerRouteServiceImpl { + fn new(my_peer_id: PeerId, global_ctx: ArcGlobalCtx) -> Self { + PeerRouteServiceImpl { + my_peer_id, + my_peer_route_id: rand::random(), + global_ctx, + sessions: DashMap::new(), + + interface: Mutex::new(None), + + cost_calculator: std::sync::Mutex::new(Some(Box::new(DefaultRouteCostCalculator))), + + route_table: RouteTable::new(), + route_table_with_cost: RouteTable::new(), + foreign_network_owner_map: DashMap::new(), + + synced_route_info: SyncedRouteInfo { + peer_infos: DashMap::new(), + raw_peer_infos: DashMap::new(), + conn_map: DashMap::new(), + foreign_network: DashMap::new(), + }, + cached_local_conn_map: std::sync::Mutex::new(RouteConnBitmap::new()), + + last_update_my_foreign_network: AtomicCell::new(None), + } + } + + fn get_or_create_session(&self, dst_peer_id: PeerId) -> Arc { + self.sessions + .entry(dst_peer_id) + .or_insert_with(|| Arc::new(SyncRouteSession::new(self.my_peer_id, dst_peer_id))) + .value() + .clone() + } + + fn get_session(&self, dst_peer_id: PeerId) -> Option> { + self.sessions.get(&dst_peer_id).map(|x| x.value().clone()) + } + + fn remove_session(&self, dst_peer_id: PeerId) { + self.sessions.remove(&dst_peer_id); + } + + fn list_session_peers(&self) -> Vec { + self.sessions.iter().map(|x| *x.key()).collect() + } + + async fn list_peers_from_interface>(&self) -> T { + self.interface + .lock() + .await + .as_ref() + .unwrap() + .list_peers() + .await + .into_iter() + .collect() + } + + fn update_my_peer_info(&self) -> bool { + if self.synced_route_info.update_my_peer_info( + self.my_peer_id, + self.my_peer_route_id, + &self.global_ctx, + ) { + self.update_route_table_and_cached_local_conn_bitmap(); + return true; + } + false + } + + async fn update_my_conn_info(&self) -> bool { + let connected_peers: BTreeSet = self.list_peers_from_interface().await; + let updated = self + .synced_route_info + .update_my_conn_info(self.my_peer_id, connected_peers); + + if updated { + self.update_route_table_and_cached_local_conn_bitmap(); + } + + updated + } + + async fn update_my_foreign_network(&self) -> bool { + let last_time = self.last_update_my_foreign_network.load(); + if last_time.is_some() + && last_time.unwrap().elapsed().as_secs() + < use_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC) + { + return false; + } + + self.last_update_my_foreign_network + .store(Some(std::time::Instant::now())); + + let foreign_networks = self + .interface + .lock() + .await + .as_ref() + .unwrap() + .list_foreign_networks() + .await; + + let updated = self + .synced_route_info + .update_my_foreign_network(self.my_peer_id, foreign_networks); + + // do not need update owner map because we always filter out my peer id. + + updated + } + + fn update_route_table(&self) { + let mut calc_locked = self.cost_calculator.lock().unwrap(); + + calc_locked.as_mut().unwrap().begin_update(); + self.route_table.build_from_synced_info( + self.my_peer_id, + &self.synced_route_info, + NextHopPolicy::LeastHop, + calc_locked.as_mut().unwrap(), + ); + + self.route_table_with_cost.build_from_synced_info( + self.my_peer_id, + &self.synced_route_info, + NextHopPolicy::LeastCost, + calc_locked.as_mut().unwrap(), + ); + calc_locked.as_mut().unwrap().end_update(); + } + + fn update_foreign_network_owner_map(&self) { + self.foreign_network_owner_map.clear(); + for item in self.synced_route_info.foreign_network.iter() { + let key = item.key(); + let entry = item.value(); + if key.peer_id == self.my_peer_id + || !self.route_table.peer_reachable(key.peer_id) + || entry.foreign_peer_ids.is_empty() + { + continue; + } + let network_identity = NetworkIdentity { + network_name: key.network_name.clone(), + network_secret: None, + network_secret_digest: Some( + entry + .network_secret_digest + .clone() + .try_into() + .unwrap_or_default(), + ), + }; + self.foreign_network_owner_map + .entry(network_identity) + .or_insert_with(|| Vec::new()) + .push(key.peer_id); + } + } + + fn cost_calculator_need_update(&self) -> bool { + self.cost_calculator + .lock() + .unwrap() + .as_ref() + .map(|x| x.need_update()) + .unwrap_or(false) + } + + fn update_route_table_and_cached_local_conn_bitmap(&self) { + // update route table first because we want to filter out unreachable peers. + self.update_route_table(); + + // the conn_bitmap should contain complete list of directly connected peers. + // use union of dst peers can preserve this property. + let all_dst_peer_ids = self + .synced_route_info + .conn_map + .iter() + .map(|x| x.value().clone().0.into_iter()) + .flatten() + .collect::>(); + + let all_peer_ids = self + .synced_route_info + .conn_map + .iter() + .map(|x| (*x.key(), x.value().1.get())) + // do not sync conn info of peers that are not reachable from any peer. + .filter(|p| all_dst_peer_ids.contains(&p.0) || self.route_table.peer_reachable(p.0)) + .collect::>(); + + let mut conn_bitmap = RouteConnBitmap::new(); + conn_bitmap.bitmap = vec![0; (all_peer_ids.len() * all_peer_ids.len() + 7) / 8]; + conn_bitmap.peer_ids = all_peer_ids; + + let all_peer_ids = &conn_bitmap.peer_ids; + for (peer_idx, (peer_id, _)) in all_peer_ids.iter().enumerate() { + let connected = self.synced_route_info.conn_map.get(peer_id).unwrap(); + + for (idx, (other_peer_id, _)) in all_peer_ids.iter().enumerate() { + if connected.0.contains(other_peer_id) { + let bit_idx = peer_idx * all_peer_ids.len() + idx; + conn_bitmap.bitmap[bit_idx / 8] |= 1 << (bit_idx % 8); + } + } + } + + *self.cached_local_conn_map.lock().unwrap() = conn_bitmap; + } + + fn build_route_info(&self, session: &SyncRouteSession) -> Option> { + let mut route_infos = Vec::new(); + for item in self.synced_route_info.peer_infos.iter() { + if session + .check_saved_peer_info_update_to_date(item.value().peer_id, item.value().version) + { + continue; + } + + // do not send unreachable peer info to dst peer. + if !self.route_table.peer_reachable(*item.key()) { + continue; + } + + route_infos.push(item.value().clone()); + } + + if route_infos.is_empty() { + None + } else { + Some(route_infos) + } + } + + fn build_conn_bitmap(&self, session: &SyncRouteSession) -> Option { + let mut need_update = false; + for (peer_id, local_version) in self.cached_local_conn_map.lock().unwrap().peer_ids.iter() { + let peer_version = session + .dst_saved_conn_bitmap_version + .get(&peer_id) + .map(|item| item.get()); + if Some(*local_version) != peer_version { + need_update = true; + break; + } + } + + if !need_update { + return None; + } + + Some(self.cached_local_conn_map.lock().unwrap().clone()) + } + + fn build_foreign_network_info( + &self, + session: &SyncRouteSession, + ) -> Option { + let mut foreign_networks = RouteForeignNetworkInfos::default(); + for item in self.synced_route_info.foreign_network.iter() { + if session + .dst_saved_foreign_network_versions + .get(&item.key()) + .map(|x| x.get() >= item.value().version) + .unwrap_or(false) + { + continue; + } + + foreign_networks + .infos + .push(route_foreign_network_infos::Info { + key: Some(item.key().clone()), + value: Some(item.value().clone()), + }); + } + + if foreign_networks.infos.is_empty() { + None + } else { + Some(foreign_networks) + } + } + + async fn update_my_infos(&self) -> bool { + let my_peer_info_updated = self.update_my_peer_info(); + let my_conn_info_updated = self.update_my_conn_info().await; + let my_foreign_network_updated = self.update_my_foreign_network().await; + if my_conn_info_updated || my_peer_info_updated { + self.update_foreign_network_owner_map(); + } + my_peer_info_updated || my_conn_info_updated || my_foreign_network_updated + } + + fn build_sync_request( + &self, + session: &SyncRouteSession, + ) -> ( + Option>, + Option, + Option, + ) { + let route_infos = self.build_route_info(&session); + let conn_bitmap = self.build_conn_bitmap(&session); + let foreign_network = self.build_foreign_network_info(&session); + + (route_infos, conn_bitmap, foreign_network) + } + + fn clear_expired_peer(&self) { + let now = SystemTime::now(); + let mut to_remove = Vec::new(); + for item in self.synced_route_info.peer_infos.iter() { + if let Ok(d) = now.duration_since(item.value().last_update.unwrap().try_into().unwrap()) + { + if d > REMOVE_DEAD_PEER_INFO_AFTER { + to_remove.push(*item.key()); + } + } + } + + for p in to_remove.iter() { + self.synced_route_info.remove_peer(*p); + } + + // clear expired foreign network info + let mut to_remove = Vec::new(); + for item in self.synced_route_info.foreign_network.iter() { + let Some(since_last_update) = item + .value() + .last_update + .and_then(|x| SystemTime::try_from(x).ok()) + .and_then(|x| now.duration_since(x).ok()) + else { + to_remove.push(item.key().clone()); + continue; + }; + + if since_last_update > REMOVE_DEAD_PEER_INFO_AFTER { + to_remove.push(item.key().clone()); + } + } + + for p in to_remove.iter() { + self.synced_route_info.foreign_network.remove(p); + } + } + + fn build_sync_route_raw_req( + req: &SyncRouteInfoRequest, + raw_peer_infos: &DashMap, + ) -> DynamicMessage { + use prost_reflect::Value; + + let mut req_dynamic_msg = DynamicMessage::new(SyncRouteInfoRequest::default().descriptor()); + req_dynamic_msg.transcode_from(req).unwrap(); + + let peer_infos = req.peer_infos.as_ref().map(|x| &x.items); + if let Some(peer_infos) = peer_infos { + let mut peer_info_raws = Vec::new(); + for peer_info in peer_infos.iter() { + if let Some(info) = raw_peer_infos.get(&peer_info.peer_id) { + peer_info_raws.push(Value::Message(info.clone())); + } else { + let mut p = DynamicMessage::new(RoutePeerInfo::default().descriptor()); + p.transcode_from(peer_info).unwrap(); + peer_info_raws.push(Value::Message(p)); + } + } + + let mut peer_infos = DynamicMessage::new(RoutePeerInfos::default().descriptor()); + peer_infos.set_field_by_name("items", Value::List(peer_info_raws)); + + req_dynamic_msg.set_field_by_name("peer_infos", Value::Message(peer_infos)); + } + + tracing::trace!(?req_dynamic_msg, "build_sync_route_raw_req"); + + req_dynamic_msg + } + + async fn sync_route_with_peer( + &self, + dst_peer_id: PeerId, + peer_rpc: Arc, + sync_as_initiator: bool, + ) -> bool { + let Some(session) = self.get_session(dst_peer_id) else { + // if session not exist, exit the sync loop. + return true; + }; + + let my_peer_id = self.my_peer_id; + + let (peer_infos, conn_bitmap, foreign_network) = self.build_sync_request(&session); + if peer_infos.is_none() + && conn_bitmap.is_none() + && foreign_network.is_none() + && !session.need_sync_initiator_info.load(Ordering::Relaxed) + && !(sync_as_initiator && session.we_are_initiator.load(Ordering::Relaxed)) + { + return true; + } + + tracing::debug!(?foreign_network, "sync_route request need send to peer. my_id {:?}, pper_id: {:?}, peer_infos: {:?}, conn_bitmap: {:?}, synced_route_info: {:?} session: {:?}", + my_peer_id, dst_peer_id, peer_infos, conn_bitmap, self.synced_route_info, session); + + session + .need_sync_initiator_info + .store(false, Ordering::Relaxed); + + let rpc_stub = peer_rpc + .rpc_client() + .scoped_client::>( + self.my_peer_id, + dst_peer_id, + self.global_ctx.get_network_name(), + ); + + let sync_route_info_req = SyncRouteInfoRequest { + my_peer_id, + my_session_id: session.my_session_id.load(Ordering::Relaxed), + is_initiator: session.we_are_initiator.load(Ordering::Relaxed), + peer_infos: peer_infos.clone().map(|x| RoutePeerInfos { items: x }), + conn_bitmap: conn_bitmap.clone().map(Into::into), + foreign_network_infos: foreign_network.clone(), + }; + + let mut ctrl = BaseController::default(); + ctrl.set_timeout_ms(3000); + ctrl.set_raw_input( + Self::build_sync_route_raw_req( + &sync_route_info_req, + &self.synced_route_info.raw_peer_infos, + ) + .encode_to_vec() + .into(), + ); + let ret = rpc_stub + .sync_route_info(ctrl, SyncRouteInfoRequest::default()) + .await; + + if let Err(e) = &ret { + tracing::error!( + ?ret, + ?my_peer_id, + ?dst_peer_id, + ?e, + "sync_route_info failed" + ); + session + .need_sync_initiator_info + .store(true, Ordering::Relaxed); + } else { + let resp = ret.as_ref().unwrap(); + if resp.error.is_some() { + let err = resp.error.unwrap(); + if err == Error::DuplicatePeerId as i32 { + if !self.global_ctx.get_feature_flags().is_public_server { + panic!("duplicate peer id"); + } + } else { + tracing::error!(?ret, ?my_peer_id, ?dst_peer_id, "sync_route_info failed"); + session + .need_sync_initiator_info + .store(true, Ordering::Relaxed); + } + } else { + session.rpc_tx_count.fetch_add(1, Ordering::Relaxed); + + session + .dst_is_initiator + .store(resp.is_initiator, Ordering::Relaxed); + + session.update_dst_session_id(resp.session_id); + + if let Some(peer_infos) = &peer_infos { + session.update_dst_saved_peer_info_version(&peer_infos); + } + + if let Some(conn_bitmap) = &conn_bitmap { + session.update_dst_saved_conn_bitmap_version(&conn_bitmap); + } + + if let Some(foreign_network) = &foreign_network { + session.update_dst_saved_foreign_network_version(&foreign_network); + } + } + } + return false; + } +} + +impl Drop for PeerRouteServiceImpl { + fn drop(&mut self) { + tracing::debug!(?self, "drop PeerRouteServiceImpl"); + } +} + +#[derive(Clone)] +struct RouteSessionManager { + service_impl: Weak, + peer_rpc: Weak, + + sync_now_broadcast: tokio::sync::broadcast::Sender<()>, +} + +impl Debug for RouteSessionManager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RouteSessionManager") + .field("dump_sessions", &self.dump_sessions()) + .finish() + } +} + +fn get_raw_peer_infos(req_raw_input: &mut bytes::Bytes) -> Option> { + let sync_req_dynamic_msg = + DynamicMessage::decode(SyncRouteInfoRequest::default().descriptor(), req_raw_input) + .unwrap(); + + let peer_infos = sync_req_dynamic_msg.get_field_by_name("peer_infos")?; + + let infos = peer_infos + .as_message()? + .get_field_by_name("items")? + .as_list()? + .iter() + .map(|x| x.as_message().unwrap().clone()) + .collect(); + + Some(infos) +} + +#[async_trait::async_trait] +impl OspfRouteRpc for RouteSessionManager { + type Controller = BaseController; + async fn sync_route_info( + &self, + ctrl: BaseController, + request: SyncRouteInfoRequest, + ) -> Result { + let from_peer_id = request.my_peer_id; + let from_session_id = request.my_session_id; + let is_initiator = request.is_initiator; + let peer_infos = request.peer_infos.map(|x| x.items); + let conn_bitmap = request.conn_bitmap.map(Into::into); + let foreign_network = request.foreign_network_infos; + let raw_peer_infos = if peer_infos.is_some() { + let r = get_raw_peer_infos(&mut ctrl.get_raw_input().unwrap()).unwrap(); + assert_eq!(r.len(), peer_infos.as_ref().unwrap().len()); + Some(r) + } else { + None + }; + + let ret = self + .do_sync_route_info( + from_peer_id, + from_session_id, + is_initiator, + peer_infos, + raw_peer_infos, + conn_bitmap, + foreign_network, + ) + .await; + + Ok(match ret { + Ok(v) => v, + Err(e) => { + let mut resp = SyncRouteInfoResponse::default(); + resp.error = Some(e as i32); + resp + } + }) + } +} + +impl RouteSessionManager { + fn new(service_impl: Arc, peer_rpc: Arc) -> Self { + RouteSessionManager { + service_impl: Arc::downgrade(&service_impl), + peer_rpc: Arc::downgrade(&peer_rpc), + + sync_now_broadcast: tokio::sync::broadcast::channel(100).0, + } + } + + async fn session_task( + peer_rpc: Weak, + service_impl: Weak, + dst_peer_id: PeerId, + mut sync_now: tokio::sync::broadcast::Receiver<()>, + ) { + let mut last_sync = Instant::now(); + loop { + loop { + let Some(service_impl) = service_impl.clone().upgrade() else { + return; + }; + + let Some(peer_rpc) = peer_rpc.clone().upgrade() else { + return; + }; + + // if we are initiator, we should ensure the dst has the session. + let sync_as_initiator = if last_sync.elapsed().as_secs() > 10 { + last_sync = Instant::now(); + true + } else { + false + }; + + if service_impl + .sync_route_with_peer(dst_peer_id, peer_rpc.clone(), sync_as_initiator) + .await + { + break; + } + + drop(service_impl); + drop(peer_rpc); + + tokio::time::sleep(Duration::from_millis(50)).await; + } + + sync_now = sync_now.resubscribe(); + + select! { + _ = tokio::time::sleep(Duration::from_secs(1)) => {} + ret = sync_now.recv() => match ret { + Err(e) => { + tracing::debug!(?e, "session_task sync_now recv failed, ospf route may exit"); + break; + }, + _ => {} + } + } + } + } + + fn stop_session(&self, peer_id: PeerId) -> Result<(), Error> { + tracing::warn!(?peer_id, "stop ospf sync session"); + let Some(service_impl) = self.service_impl.upgrade() else { + return Err(Error::Stopped); + }; + service_impl.remove_session(peer_id); + Ok(()) + } + + fn start_session_task(&self, session: &SyncRouteSession) { + if !session.task.is_running() { + session.task.set_task(tokio::spawn(Self::session_task( + self.peer_rpc.clone(), + self.service_impl.clone(), + session.dst_peer_id, + self.sync_now_broadcast.subscribe(), + ))); + } + } + + fn get_or_start_session(&self, peer_id: PeerId) -> Result, Error> { + let Some(service_impl) = self.service_impl.upgrade() else { + return Err(Error::Stopped); + }; + + tracing::info!(?service_impl.my_peer_id, ?peer_id, "start ospf sync session"); + + let session = service_impl.get_or_create_session(peer_id); + self.start_session_task(&session); + Ok(session) + } + + async fn maintain_sessions(&self, service_impl: Arc) -> bool { + let mut cur_dst_peer_id_to_initiate = None; + let mut next_sleep_ms = 0; + loop { + let mut recv = self.sync_now_broadcast.subscribe(); + select! { + _ = tokio::time::sleep(Duration::from_millis(next_sleep_ms)) => {} + _ = recv.recv() => {} + } + + let mut peers = service_impl.list_peers_from_interface::>().await; + peers.sort(); + + let session_peers = self.list_session_peers(); + for peer_id in session_peers.iter() { + if !peers.contains(peer_id) { + if Some(*peer_id) == cur_dst_peer_id_to_initiate { + cur_dst_peer_id_to_initiate = None; + } + let _ = self.stop_session(*peer_id); + } + } + + // find peer_ids that are not initiators. + let initiator_candidates = peers + .iter() + .filter(|x| { + let Some(session) = service_impl.get_session(**x) else { + return true; + }; + !session.dst_is_initiator.load(Ordering::Relaxed) + }) + .map(|x| *x) + .collect::>(); + + if initiator_candidates.is_empty() { + next_sleep_ms = 1000; + continue; + } + + let mut new_initiator_dst = None; + // if any peer has NoPAT or OpenInternet stun type, we should use it. + for peer_id in initiator_candidates.iter() { + let Some(nat_type) = service_impl.route_table.get_nat_type(*peer_id) else { + continue; + }; + if nat_type == NatType::NoPat || nat_type == NatType::OpenInternet { + new_initiator_dst = Some(*peer_id); + break; + } + } + if new_initiator_dst.is_none() { + new_initiator_dst = Some(*initiator_candidates.first().unwrap()); + } + + if new_initiator_dst != cur_dst_peer_id_to_initiate { + tracing::warn!( + "new_initiator: {:?}, prev: {:?}, my_id: {:?}", + new_initiator_dst, + cur_dst_peer_id_to_initiate, + service_impl.my_peer_id + ); + // update initiator flag for previous session + if let Some(cur_peer_id_to_initiate) = cur_dst_peer_id_to_initiate { + if let Some(session) = service_impl.get_session(cur_peer_id_to_initiate) { + session.update_initiator_flag(false); + } + } + + cur_dst_peer_id_to_initiate = new_initiator_dst; + // update initiator flag for new session + let Ok(session) = self.get_or_start_session(new_initiator_dst.unwrap()) else { + tracing::warn!("get_or_start_session failed"); + continue; + }; + session.update_initiator_flag(true); + } + + // clear sessions that are neither dst_initiator or we_are_initiator. + for peer_id in session_peers.iter() { + if let Some(session) = service_impl.get_session(*peer_id) { + if (session.dst_is_initiator.load(Ordering::Relaxed) + || session.we_are_initiator.load(Ordering::Relaxed) + || session.need_sync_initiator_info.load(Ordering::Relaxed)) + && session.task.is_running() + { + continue; + } + let _ = self.stop_session(*peer_id); + } + } + + next_sleep_ms = 1000; + } + } + + fn list_session_peers(&self) -> Vec { + let Some(service_impl) = self.service_impl.upgrade() else { + return vec![]; + }; + + service_impl.list_session_peers() + } + + fn dump_sessions(&self) -> Result { + let Some(service_impl) = self.service_impl.upgrade() else { + return Err(Error::Stopped); + }; + + let mut ret = format!("my_peer_id: {:?}\n", service_impl.my_peer_id); + for item in service_impl.sessions.iter() { + ret += format!( + " session: {}, {}\n", + item.key(), + item.value().short_debug_string() + ) + .as_str(); + } + + Ok(ret.to_string()) + } + + fn sync_now(&self, reason: &str) { + let ret = self.sync_now_broadcast.send(()); + tracing::debug!(?ret, ?reason, "sync_now_broadcast.send"); + } + + async fn do_sync_route_info( + &self, + from_peer_id: PeerId, + from_session_id: SessionId, + is_initiator: bool, + peer_infos: Option>, + raw_peer_infos: Option>, + conn_bitmap: Option, + foreign_network: Option, + ) -> Result { + let Some(service_impl) = self.service_impl.upgrade() else { + return Err(Error::Stopped); + }; + + let my_peer_id = service_impl.my_peer_id; + let session = self.get_or_start_session(from_peer_id)?; + + session.rpc_rx_count.fetch_add(1, Ordering::Relaxed); + + session.update_dst_session_id(from_session_id); + + let mut need_update_route_table = false; + + if let Some(peer_infos) = &peer_infos { + service_impl.synced_route_info.update_peer_infos( + my_peer_id, + service_impl.my_peer_route_id, + from_peer_id, + peer_infos, + raw_peer_infos.as_ref().unwrap(), + )?; + session.update_dst_saved_peer_info_version(peer_infos); + need_update_route_table = true; + } + + if let Some(conn_bitmap) = &conn_bitmap { + service_impl.synced_route_info.update_conn_map(&conn_bitmap); + session.update_dst_saved_conn_bitmap_version(conn_bitmap); + need_update_route_table = true; + } + + if need_update_route_table { + service_impl.update_route_table_and_cached_local_conn_bitmap(); + } + + if let Some(foreign_network) = &foreign_network { + service_impl + .synced_route_info + .update_foreign_network(&foreign_network); + session.update_dst_saved_foreign_network_version(foreign_network); + } + + if need_update_route_table || foreign_network.is_some() { + service_impl.update_foreign_network_owner_map(); + } + + tracing::info!( + "handling sync_route_info rpc: from_peer_id: {:?}, is_initiator: {:?}, peer_infos: {:?}, conn_bitmap: {:?}, synced_route_info: {:?} session: {:?}, new_route_table: {:?}", + from_peer_id, is_initiator, peer_infos, conn_bitmap, service_impl.synced_route_info, session, service_impl.route_table); + + session + .dst_is_initiator + .store(is_initiator, Ordering::Relaxed); + let is_initiator = session.we_are_initiator.load(Ordering::Relaxed); + let session_id = session.my_session_id.load(Ordering::Relaxed); + + self.sync_now("sync_route_info"); + + Ok(SyncRouteInfoResponse { + is_initiator, + session_id, + error: None, + }) + } +} + +pub struct PeerRoute { + my_peer_id: PeerId, + global_ctx: ArcGlobalCtx, + peer_rpc: Weak, + + service_impl: Arc, + session_mgr: RouteSessionManager, + + tasks: std::sync::Mutex>, +} + +impl Debug for PeerRoute { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PeerRoute") + .field("my_peer_id", &self.my_peer_id) + .field("service_impl", &self.service_impl) + .field("session_mgr", &self.session_mgr) + .finish() + } +} + +impl PeerRoute { + pub fn new( + my_peer_id: PeerId, + global_ctx: ArcGlobalCtx, + peer_rpc: Arc, + ) -> Arc { + let service_impl = Arc::new(PeerRouteServiceImpl::new(my_peer_id, global_ctx.clone())); + let session_mgr = RouteSessionManager::new(service_impl.clone(), peer_rpc.clone()); + + Arc::new(PeerRoute { + my_peer_id, + global_ctx: global_ctx.clone(), + peer_rpc: Arc::downgrade(&peer_rpc), + + service_impl, + session_mgr, + + tasks: std::sync::Mutex::new(JoinSet::new()), + }) + } + + async fn clear_expired_peer(service_impl: Arc) { + loop { + tokio::time::sleep(Duration::from_secs(60)).await; + service_impl.clear_expired_peer(); + // TODO: use debug log level for this. + tracing::debug!(?service_impl, "clear_expired_peer"); + } + } + + #[tracing::instrument(skip(session_mgr))] + async fn maintain_session_tasks( + session_mgr: RouteSessionManager, + service_impl: Arc, + ) { + session_mgr.maintain_sessions(service_impl).await; + } + + #[tracing::instrument(skip(session_mgr))] + async fn update_my_peer_info_routine( + service_impl: Arc, + session_mgr: RouteSessionManager, + ) { + let mut global_event_receiver = service_impl.global_ctx.subscribe(); + loop { + if service_impl.update_my_infos().await { + session_mgr.sync_now("update_my_infos"); + } + + if service_impl.cost_calculator_need_update() { + tracing::debug!("cost_calculator_need_update"); + service_impl.update_route_table(); + } + + select! { + ev = global_event_receiver.recv() => { + tracing::info!(?ev, "global event received in update_my_peer_info_routine"); + } + _ = tokio::time::sleep(Duration::from_secs(1)) => {} + } + } + } + + async fn start(&self) { + let Some(peer_rpc) = self.peer_rpc.upgrade() else { + return; + }; + + // make sure my_peer_id is in the peer_infos. + self.service_impl.update_my_infos().await; + + peer_rpc.rpc_server().registry().register( + OspfRouteRpcServer::new(self.session_mgr.clone()), + &self.global_ctx.get_network_name(), + ); + + self.tasks + .lock() + .unwrap() + .spawn(Self::update_my_peer_info_routine( + self.service_impl.clone(), + self.session_mgr.clone(), + )); + + self.tasks + .lock() + .unwrap() + .spawn(Self::maintain_session_tasks( + self.session_mgr.clone(), + self.service_impl.clone(), + )); + + self.tasks + .lock() + .unwrap() + .spawn(Self::clear_expired_peer(self.service_impl.clone())); + } +} + +impl Drop for PeerRoute { + fn drop(&mut self) { + tracing::debug!( + self.my_peer_id, + network = ?self.global_ctx.get_network_identity(), + service = ?self.service_impl, + "PeerRoute drop" + ); + + let Some(peer_rpc) = self.peer_rpc.upgrade() else { + return; + }; + + peer_rpc.rpc_server().registry().unregister( + OspfRouteRpcServer::new(self.session_mgr.clone()), + &self.global_ctx.get_network_name(), + ); + } +} + +#[async_trait::async_trait] +impl Route for PeerRoute { + async fn open(&self, interface: RouteInterfaceBox) -> Result { + *self.service_impl.interface.lock().await = Some(interface); + self.start().await; + Ok(1) + } + + async fn close(&self) {} + + async fn get_next_hop(&self, dst_peer_id: PeerId) -> Option { + let route_table = &self.service_impl.route_table; + route_table + .get_next_hop(dst_peer_id) + .map(|x| x.next_hop_peer_id) + } + + async fn get_next_hop_with_policy( + &self, + dst_peer_id: PeerId, + policy: NextHopPolicy, + ) -> Option { + let route_table = if matches!(policy, NextHopPolicy::LeastCost) { + &self.service_impl.route_table_with_cost + } else { + &self.service_impl.route_table + }; + route_table + .get_next_hop(dst_peer_id) + .map(|x| x.next_hop_peer_id) + } + + async fn list_routes(&self) -> Vec { + let route_table = &self.service_impl.route_table; + let route_table_with_cost = &self.service_impl.route_table_with_cost; + let mut routes = Vec::new(); + for item in route_table.peer_infos.iter() { + if *item.key() == self.my_peer_id { + continue; + } + let Some(next_hop_peer) = route_table.get_next_hop(*item.key()) else { + continue; + }; + let next_hop_peer_latency_first = route_table_with_cost.get_next_hop(*item.key()); + let mut route: crate::proto::cli::Route = item.value().clone().into(); + route.next_hop_peer_id = next_hop_peer.next_hop_peer_id; + route.cost = (next_hop_peer.path_len - 1) as i32; + route.path_latency = next_hop_peer.path_latency; + + route.next_hop_peer_id_latency_first = + next_hop_peer_latency_first.map(|x| x.next_hop_peer_id); + route.cost_latency_first = next_hop_peer_latency_first.map(|x| x.path_latency); + route.path_latency_latency_first = next_hop_peer_latency_first.map(|x| x.path_latency); + + route.feature_flag = item.feature_flag.clone(); + + routes.push(route); + } + routes + } + + async fn get_peer_id_by_ipv4(&self, ipv4_addr: &Ipv4Addr) -> Option { + let route_table = &self.service_impl.route_table; + if let Some(peer_id) = route_table.ipv4_peer_id_map.get(ipv4_addr) { + return Some(*peer_id); + } + + if let Some(peer_id) = route_table.get_peer_id_for_proxy(ipv4_addr) { + return Some(peer_id); + } + + tracing::debug!(?ipv4_addr, "no peer id for ipv4"); + None + } + + async fn set_route_cost_fn(&self, _cost_fn: RouteCostCalculator) { + *self.service_impl.cost_calculator.lock().unwrap() = Some(_cost_fn); + self.service_impl.update_route_table(); + } + + async fn dump(&self) -> String { + format!("{:#?}", self) + } + + async fn list_foreign_network_info(&self) -> RouteForeignNetworkInfos { + let route_table = &self.service_impl.route_table; + let mut foreign_networks = RouteForeignNetworkInfos::default(); + for item in self + .service_impl + .synced_route_info + .foreign_network + .iter() + .filter(|x| !x.value().foreign_peer_ids.is_empty()) + .filter(|x| route_table.peer_reachable(x.key().peer_id)) + { + foreign_networks + .infos + .push(route_foreign_network_infos::Info { + key: Some(item.key().clone()), + value: Some(item.value().clone()), + }); + } + foreign_networks + } + + async fn list_peers_own_foreign_network( + &self, + network_identity: &NetworkIdentity, + ) -> Vec { + self.service_impl + .foreign_network_owner_map + .get(network_identity) + .map(|x| x.clone()) + .unwrap_or_default() + } + + async fn get_feature_flag(&self, peer_id: PeerId) -> Option { + self.service_impl + .route_table + .peer_infos + .get(&peer_id) + .and_then(|x| x.feature_flag.clone()) + } +} + +impl PeerPacketFilter for Arc {} + +#[cfg(test)] +mod tests { + use std::{ + collections::BTreeSet, + sync::{atomic::Ordering, Arc}, + time::Duration, + }; + + use dashmap::DashMap; + use prost_reflect::{DynamicMessage, ReflectMessage}; + + use crate::{ + common::{global_ctx::tests::get_mock_global_ctx, PeerId}, + connector::udp_hole_punch::tests::replace_stun_info_collector, + peers::{ + create_packet_recv_chan, + peer_manager::{PeerManager, RouteAlgoType}, + peer_ospf_route::PeerRouteServiceImpl, + route_trait::{NextHopPolicy, Route, RouteCostCalculatorInterface}, + tests::connect_peer_manager, + }, + proto::{ + common::NatType, + peer_rpc::{RoutePeerInfo, RoutePeerInfos, SyncRouteInfoRequest}, + }, + tunnel::common::tests::wait_for_condition, + }; + use prost::Message; + + use super::PeerRoute; + + async fn create_mock_route(peer_mgr: Arc) -> Arc { + let peer_route = PeerRoute::new( + peer_mgr.my_peer_id(), + peer_mgr.get_global_ctx(), + peer_mgr.get_peer_rpc_mgr(), + ); + peer_mgr.add_route(peer_route.clone()).await; + peer_route + } + + fn get_rpc_counter(route: &Arc, peer_id: PeerId) -> (u32, u32) { + let session = route.service_impl.get_session(peer_id).unwrap(); + ( + session.rpc_tx_count.load(Ordering::Relaxed), + session.rpc_rx_count.load(Ordering::Relaxed), + ) + } + + fn get_is_initiator(route: &Arc, peer_id: PeerId) -> (bool, bool) { + let session = route.service_impl.get_session(peer_id).unwrap(); + ( + session.we_are_initiator.load(Ordering::Relaxed), + session.dst_is_initiator.load(Ordering::Relaxed), + ) + } + + async fn create_mock_pmgr() -> Arc { + let (s, _r) = create_packet_recv_chan(); + let peer_mgr = Arc::new(PeerManager::new( + RouteAlgoType::None, + get_mock_global_ctx(), + s, + )); + replace_stun_info_collector(peer_mgr.clone(), NatType::Unknown); + peer_mgr.run().await.unwrap(); + peer_mgr + } + + fn check_rpc_counter(route: &Arc, peer_id: PeerId, max_tx: u32, max_rx: u32) { + let (tx1, rx1) = get_rpc_counter(route, peer_id); + assert!(tx1 <= max_tx); + assert!(rx1 <= max_rx); + } + + #[tokio::test] + async fn ospf_route_2node() { + let p_a = create_mock_pmgr().await; + let p_b = create_mock_pmgr().await; + connect_peer_manager(p_a.clone(), p_b.clone()).await; + + let r_a = create_mock_route(p_a.clone()).await; + let r_b = create_mock_route(p_b.clone()).await; + + for r in vec![r_a.clone(), r_b.clone()].iter() { + wait_for_condition( + || async { r.list_routes().await.len() == 1 }, + Duration::from_secs(5), + ) + .await; + } + + tokio::time::sleep(Duration::from_secs(3)).await; + + assert_eq!(2, r_a.service_impl.synced_route_info.peer_infos.len()); + assert_eq!(2, r_b.service_impl.synced_route_info.peer_infos.len()); + + for s in r_a.service_impl.sessions.iter() { + assert!(s.value().task.is_running()); + } + + assert_eq!( + r_a.service_impl + .synced_route_info + .peer_infos + .get(&p_a.my_peer_id()) + .unwrap() + .version, + r_a.service_impl + .get_session(p_b.my_peer_id()) + .unwrap() + .dst_saved_peer_info_versions + .get(&p_a.my_peer_id()) + .unwrap() + .value() + .0 + .load(Ordering::Relaxed) + ); + + assert_eq!((1, 1), get_rpc_counter(&r_a, p_b.my_peer_id())); + assert_eq!((1, 1), get_rpc_counter(&r_b, p_a.my_peer_id())); + + let i_a = get_is_initiator(&r_a, p_b.my_peer_id()); + let i_b = get_is_initiator(&r_b, p_a.my_peer_id()); + assert_eq!(i_a.0, i_b.1); + assert_eq!(i_b.0, i_a.1); + + drop(r_b); + drop(p_b); + + wait_for_condition( + || async { r_a.list_routes().await.len() == 0 }, + Duration::from_secs(5), + ) + .await; + + wait_for_condition( + || async { r_a.service_impl.sessions.is_empty() }, + Duration::from_secs(5), + ) + .await; + } + + #[tokio::test] + async fn ospf_route_multi_node() { + let p_a = create_mock_pmgr().await; + let p_b = create_mock_pmgr().await; + let p_c = create_mock_pmgr().await; + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_c.clone(), p_b.clone()).await; + + let r_a = create_mock_route(p_a.clone()).await; + let r_b = create_mock_route(p_b.clone()).await; + let r_c = create_mock_route(p_c.clone()).await; + + for r in vec![r_a.clone(), r_b.clone(), r_c.clone()].iter() { + wait_for_condition( + || async { r.service_impl.synced_route_info.peer_infos.len() == 3 }, + Duration::from_secs(5), + ) + .await; + } + + connect_peer_manager(p_a.clone(), p_c.clone()).await; + // for full-connected 3 nodes, the sessions between them may be a cycle or a line + wait_for_condition( + || async { + let mut lens = vec![ + r_a.service_impl.sessions.len(), + r_b.service_impl.sessions.len(), + r_c.service_impl.sessions.len(), + ]; + lens.sort(); + + lens == vec![1, 1, 2] || lens == vec![2, 2, 2] + }, + Duration::from_secs(3), + ) + .await; + + let p_d = create_mock_pmgr().await; + let r_d = create_mock_route(p_d.clone()).await; + connect_peer_manager(p_d.clone(), p_a.clone()).await; + connect_peer_manager(p_d.clone(), p_b.clone()).await; + connect_peer_manager(p_d.clone(), p_c.clone()).await; + + // find the smallest peer_id, which should be a center node + let mut all_route = vec![r_a.clone(), r_b.clone(), r_c.clone(), r_d.clone()]; + all_route.sort_by(|a, b| a.my_peer_id.cmp(&b.my_peer_id)); + let mut all_peer_mgr = vec![p_a.clone(), p_b.clone(), p_c.clone(), p_d.clone()]; + all_peer_mgr.sort_by(|a, b| a.my_peer_id().cmp(&b.my_peer_id())); + + wait_for_condition( + || async { all_route[0].service_impl.sessions.len() == 3 }, + Duration::from_secs(3), + ) + .await; + + for r in all_route.iter() { + println!("session: {}", r.session_mgr.dump_sessions().unwrap()); + } + + let p_e = create_mock_pmgr().await; + let r_e = create_mock_route(p_e.clone()).await; + let last_p = all_peer_mgr.last().unwrap(); + connect_peer_manager(p_e.clone(), last_p.clone()).await; + + wait_for_condition( + || async { r_e.session_mgr.list_session_peers().len() == 1 }, + Duration::from_secs(3), + ) + .await; + + for s in r_e.service_impl.sessions.iter() { + assert!(s.value().task.is_running()); + } + + tokio::time::sleep(Duration::from_secs(2)).await; + + check_rpc_counter(&r_e, last_p.my_peer_id(), 2, 2); + + for r in all_route.iter() { + if r.my_peer_id != last_p.my_peer_id() { + wait_for_condition( + || async { + r.get_next_hop(p_e.my_peer_id()).await == Some(last_p.my_peer_id()) + }, + Duration::from_secs(3), + ) + .await; + } else { + wait_for_condition( + || async { r.get_next_hop(p_e.my_peer_id()).await == Some(p_e.my_peer_id()) }, + Duration::from_secs(3), + ) + .await; + } + } + } + + async fn check_route_sanity(p: &Arc, routable_peers: Vec>) { + let synced_info = &p.service_impl.synced_route_info; + for routable_peer in routable_peers.iter() { + // check conn map + let conns = synced_info + .conn_map + .get(&routable_peer.my_peer_id()) + .unwrap(); + + assert_eq!( + conns.0, + routable_peer + .get_peer_map() + .list_peers() + .await + .into_iter() + .collect::>() + ); + + // check peer infos + let peer_info = synced_info + .peer_infos + .get(&routable_peer.my_peer_id()) + .unwrap(); + assert_eq!(peer_info.peer_id, routable_peer.my_peer_id()); + } + } + + async fn print_routes(peers: Vec>) { + for p in peers.iter() { + println!("p:{:?}, route: {:#?}", p.my_peer_id, p.list_routes().await); + } + } + + #[tokio::test] + async fn ospf_route_3node_disconnect() { + let p_a = create_mock_pmgr().await; + let p_b = create_mock_pmgr().await; + let p_c = create_mock_pmgr().await; + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_c.clone(), p_b.clone()).await; + + let mgrs = vec![p_a.clone(), p_b.clone(), p_c.clone()]; + + let r_a = create_mock_route(p_a.clone()).await; + let r_b = create_mock_route(p_b.clone()).await; + let r_c = create_mock_route(p_c.clone()).await; + + for r in vec![r_a.clone(), r_b.clone(), r_c.clone()].iter() { + wait_for_condition( + || async { r.service_impl.synced_route_info.peer_infos.len() == 3 }, + Duration::from_secs(5), + ) + .await; + } + + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + print_routes(vec![r_a.clone(), r_b.clone(), r_c.clone()]).await; + check_route_sanity(&r_a, mgrs.clone()).await; + check_route_sanity(&r_b, mgrs.clone()).await; + check_route_sanity(&r_c, mgrs.clone()).await; + + assert_eq!(2, r_a.list_routes().await.len()); + + drop(mgrs); + drop(r_c); + drop(p_c); + + for r in vec![r_a.clone(), r_b.clone()].iter() { + wait_for_condition( + || async { r.list_routes().await.len() == 1 }, + Duration::from_secs(5), + ) + .await; + } + } + + #[tokio::test] + async fn peer_reconnect() { + let p_a = create_mock_pmgr().await; + let p_b = create_mock_pmgr().await; + let r_a = create_mock_route(p_a.clone()).await; + let r_b = create_mock_route(p_b.clone()).await; + + connect_peer_manager(p_a.clone(), p_b.clone()).await; + + wait_for_condition( + || async { r_a.list_routes().await.len() == 1 }, + Duration::from_secs(5), + ) + .await; + + assert_eq!(1, r_b.list_routes().await.len()); + + check_rpc_counter(&r_a, p_b.my_peer_id(), 2, 2); + + p_a.get_peer_map() + .close_peer(p_b.my_peer_id()) + .await + .unwrap(); + wait_for_condition( + || async { r_a.list_routes().await.len() == 0 }, + Duration::from_secs(5), + ) + .await; + + // reconnect + connect_peer_manager(p_a.clone(), p_b.clone()).await; + wait_for_condition( + || async { r_a.list_routes().await.len() == 1 }, + Duration::from_secs(5), + ) + .await; + + // wait session init + tokio::time::sleep(Duration::from_secs(1)).await; + + println!("session: {:?}", r_a.session_mgr.dump_sessions()); + check_rpc_counter(&r_a, p_b.my_peer_id(), 2, 2); + } + + #[tokio::test] + async fn test_cost_calculator() { + let p_a = create_mock_pmgr().await; + let p_b = create_mock_pmgr().await; + let p_c = create_mock_pmgr().await; + let p_d = create_mock_pmgr().await; + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_a.clone(), p_c.clone()).await; + connect_peer_manager(p_d.clone(), p_b.clone()).await; + connect_peer_manager(p_d.clone(), p_c.clone()).await; + connect_peer_manager(p_b.clone(), p_c.clone()).await; + + let _r_a = create_mock_route(p_a.clone()).await; + let _r_b = create_mock_route(p_b.clone()).await; + let _r_c = create_mock_route(p_c.clone()).await; + let r_d = create_mock_route(p_d.clone()).await; + + // in normal mode, packet from p_c should directly forward to p_a + wait_for_condition( + || async { r_d.get_next_hop(p_a.my_peer_id()).await != None }, + Duration::from_secs(5), + ) + .await; + + struct TestCostCalculator { + p_a_peer_id: PeerId, + p_b_peer_id: PeerId, + p_c_peer_id: PeerId, + p_d_peer_id: PeerId, + } + + impl RouteCostCalculatorInterface for TestCostCalculator { + fn calculate_cost(&self, src: PeerId, dst: PeerId) -> i32 { + if src == self.p_d_peer_id && dst == self.p_b_peer_id { + return 100; + } + + if src == self.p_d_peer_id && dst == self.p_c_peer_id { + return 1; + } + + if src == self.p_c_peer_id && dst == self.p_a_peer_id { + return 101; + } + + if src == self.p_b_peer_id && dst == self.p_a_peer_id { + return 1; + } + + if src == self.p_c_peer_id && dst == self.p_b_peer_id { + return 2; + } + + 1 + } + } + + r_d.set_route_cost_fn(Box::new(TestCostCalculator { + p_a_peer_id: p_a.my_peer_id(), + p_b_peer_id: p_b.my_peer_id(), + p_c_peer_id: p_c.my_peer_id(), + p_d_peer_id: p_d.my_peer_id(), + })) + .await; + + // after set cost, packet from p_c should forward to p_b first + wait_for_condition( + || async { + r_d.get_next_hop_with_policy(p_a.my_peer_id(), NextHopPolicy::LeastCost) + .await + == Some(p_c.my_peer_id()) + }, + Duration::from_secs(5), + ) + .await; + + wait_for_condition( + || async { + r_d.get_next_hop_with_policy(p_a.my_peer_id(), NextHopPolicy::LeastHop) + .await + == Some(p_b.my_peer_id()) + }, + Duration::from_secs(5), + ) + .await; + } + + #[tokio::test] + async fn test_raw_peer_info() { + let mut req = SyncRouteInfoRequest::default(); + let raw_info_map: DashMap = DashMap::new(); + + req.peer_infos = Some(RoutePeerInfos { + items: vec![RoutePeerInfo { + peer_id: 1, + ..Default::default() + }], + }); + + let mut raw_req = DynamicMessage::new(RoutePeerInfo::default().descriptor()); + raw_req + .transcode_from(&req.peer_infos.as_ref().unwrap().items[0]) + .unwrap(); + raw_info_map.insert(1, raw_req); + + let out = PeerRouteServiceImpl::build_sync_route_raw_req(&req, &raw_info_map); + + let out_bytes = out.encode_to_vec(); + + let req2 = SyncRouteInfoRequest::decode(out_bytes.as_slice()).unwrap(); + + assert_eq!(req, req2); + } +} diff --git a/rust/easytier/src/peers/peer_rpc.rs b/rust/easytier/src/peers/peer_rpc.rs new file mode 100644 index 0000000..739bdd8 --- /dev/null +++ b/rust/easytier/src/peers/peer_rpc.rs @@ -0,0 +1,335 @@ +use std::sync::{Arc, Mutex}; + +use futures::{SinkExt as _, StreamExt}; +use tokio::task::JoinSet; + +use crate::{ + common::{error::Error, PeerId}, + proto::rpc_impl::{self, bidirect::BidirectRpcManager}, + tunnel::packet_def::ZCPacket, +}; + +const RPC_PACKET_CONTENT_MTU: usize = 1300; + +type PeerRpcServiceId = u32; +type PeerRpcTransactId = u32; + +#[async_trait::async_trait] +#[auto_impl::auto_impl(Arc)] +pub trait PeerRpcManagerTransport: Send + Sync + 'static { + fn my_peer_id(&self) -> PeerId; + async fn send(&self, msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error>; + async fn recv(&self) -> Result; +} + +// handle rpc request from one peer +pub struct PeerRpcManager { + tspt: Arc>, + bidirect_rpc: BidirectRpcManager, + tasks: Arc>>, +} + +impl std::fmt::Debug for PeerRpcManager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PeerRpcManager") + .field("node_id", &self.tspt.my_peer_id()) + .finish() + } +} + +impl PeerRpcManager { + pub fn new(tspt: impl PeerRpcManagerTransport) -> Self { + Self { + tspt: Arc::new(Box::new(tspt)), + bidirect_rpc: BidirectRpcManager::new(), + + tasks: Arc::new(Mutex::new(JoinSet::new())), + } + } + + pub fn run(&self) { + let ret = self.bidirect_rpc.run_and_create_tunnel(); + let (mut rx, mut tx) = ret.split(); + let tspt = self.tspt.clone(); + self.tasks.lock().unwrap().spawn(async move { + while let Some(Ok(packet)) = rx.next().await { + let dst_peer_id = packet.peer_manager_header().unwrap().to_peer_id.into(); + if let Err(e) = tspt.send(packet, dst_peer_id).await { + tracing::error!("send to rpc tspt error: {:?}", e); + } + } + }); + + let tspt = self.tspt.clone(); + self.tasks.lock().unwrap().spawn(async move { + while let Ok(packet) = tspt.recv().await { + if let Err(e) = tx.send(packet).await { + tracing::error!("send to rpc tspt error: {:?}", e); + } + } + }); + } + + pub fn rpc_client(&self) -> &rpc_impl::client::Client { + self.bidirect_rpc.rpc_client() + } + + pub fn rpc_server(&self) -> &rpc_impl::server::Server { + self.bidirect_rpc.rpc_server() + } + + pub fn my_peer_id(&self) -> PeerId { + self.tspt.my_peer_id() + } +} + +impl Drop for PeerRpcManager { + fn drop(&mut self) { + tracing::debug!("PeerRpcManager drop, my_peer_id: {:?}", self.my_peer_id()); + } +} + +#[cfg(test)] +pub mod tests { + use std::{pin::Pin, sync::Arc}; + + use futures::{SinkExt, StreamExt}; + use tokio::sync::Mutex; + + use crate::{ + common::{error::Error, new_peer_id, PeerId}, + peers::{ + peer_rpc::PeerRpcManager, + tests::{connect_peer_manager, create_mock_peer_manager, wait_route_appear}, + }, + proto::{ + rpc_impl::RpcController, + tests::{GreetingClientFactory, GreetingServer, GreetingService, SayHelloRequest}, + }, + tunnel::{ + packet_def::ZCPacket, ring::create_ring_tunnel_pair, Tunnel, ZCPacketSink, + ZCPacketStream, + }, + }; + + use super::PeerRpcManagerTransport; + + fn random_string(len: usize) -> String { + use rand::distributions::Alphanumeric; + use rand::Rng; + let mut rng = rand::thread_rng(); + let s: Vec = std::iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .take(len) + .collect(); + String::from_utf8(s).unwrap() + } + + pub fn register_service(rpc_mgr: &PeerRpcManager, domain: &str, delay_ms: u64, prefix: &str) { + rpc_mgr.rpc_server().registry().register( + GreetingServer::new(GreetingService { + delay_ms, + prefix: prefix.to_string(), + }), + domain, + ); + } + + #[tokio::test] + async fn peer_rpc_basic_test() { + struct MockTransport { + sink: Arc>>>, + stream: Arc>>>, + my_peer_id: PeerId, + } + + #[async_trait::async_trait] + impl PeerRpcManagerTransport for MockTransport { + fn my_peer_id(&self) -> PeerId { + self.my_peer_id + } + async fn send(&self, msg: ZCPacket, _dst_peer_id: PeerId) -> Result<(), Error> { + println!("rpc mgr send: {:?}", msg); + self.sink.lock().await.send(msg).await.unwrap(); + Ok(()) + } + async fn recv(&self) -> Result { + let ret = self.stream.lock().await.next().await.unwrap(); + println!("rpc mgr recv: {:?}", ret); + return ret.map_err(|e| e.into()); + } + } + + let (ct, st) = create_ring_tunnel_pair(); + let (cts, ctsr) = ct.split(); + let (sts, stsr) = st.split(); + + let server_rpc_mgr = PeerRpcManager::new(MockTransport { + sink: Arc::new(Mutex::new(ctsr)), + stream: Arc::new(Mutex::new(cts)), + my_peer_id: new_peer_id(), + }); + server_rpc_mgr.run(); + register_service(&server_rpc_mgr, "test", 0, "Hello"); + + let client_rpc_mgr = PeerRpcManager::new(MockTransport { + sink: Arc::new(Mutex::new(stsr)), + stream: Arc::new(Mutex::new(sts)), + my_peer_id: new_peer_id(), + }); + client_rpc_mgr.run(); + + let stub = client_rpc_mgr + .rpc_client() + .scoped_client::>(1, 1, "test".to_string()); + + let msg = random_string(8192); + let ret = stub + .say_hello( + RpcController::default(), + SayHelloRequest { name: msg.clone() }, + ) + .await + .unwrap(); + + println!("ret: {:?}", ret); + assert_eq!(ret.greeting, format!("Hello {}!", msg)); + + let msg = random_string(10); + let ret = stub + .say_hello( + RpcController::default(), + SayHelloRequest { name: msg.clone() }, + ) + .await + .unwrap(); + + println!("ret: {:?}", ret); + assert_eq!(ret.greeting, format!("Hello {}!", msg)); + } + + #[tokio::test] + async fn test_rpc_with_peer_manager() { + let peer_mgr_a = create_mock_peer_manager().await; + let peer_mgr_b = create_mock_peer_manager().await; + let peer_mgr_c = create_mock_peer_manager().await; + connect_peer_manager(peer_mgr_a.clone(), peer_mgr_b.clone()).await; + connect_peer_manager(peer_mgr_b.clone(), peer_mgr_c.clone()).await; + + wait_route_appear(peer_mgr_a.clone(), peer_mgr_b.clone()) + .await + .unwrap(); + wait_route_appear(peer_mgr_a.clone(), peer_mgr_c.clone()) + .await + .unwrap(); + + assert_eq!(peer_mgr_a.get_peer_map().list_peers().await.len(), 1); + assert_eq!( + peer_mgr_a.get_peer_map().list_peers().await[0], + peer_mgr_b.my_peer_id() + ); + + assert_eq!(peer_mgr_c.get_peer_map().list_peers().await.len(), 1); + assert_eq!( + peer_mgr_c.get_peer_map().list_peers().await[0], + peer_mgr_b.my_peer_id() + ); + + register_service(&peer_mgr_b.get_peer_rpc_mgr(), "test", 0, "Hello"); + + let msg = random_string(16 * 1024); + let stub = peer_mgr_a + .get_peer_rpc_mgr() + .rpc_client() + .scoped_client::>( + peer_mgr_a.my_peer_id(), + peer_mgr_b.my_peer_id(), + "test".to_string(), + ); + + let ret = stub + .say_hello( + RpcController::default(), + SayHelloRequest { name: msg.clone() }, + ) + .await + .unwrap(); + assert_eq!(ret.greeting, format!("Hello {}!", msg)); + + // call again + let msg = random_string(16 * 1024); + let ret = stub + .say_hello( + RpcController::default(), + SayHelloRequest { name: msg.clone() }, + ) + .await + .unwrap(); + assert_eq!(ret.greeting, format!("Hello {}!", msg)); + + let msg = random_string(16 * 1024); + let ret = stub + .say_hello( + RpcController::default(), + SayHelloRequest { name: msg.clone() }, + ) + .await + .unwrap(); + assert_eq!(ret.greeting, format!("Hello {}!", msg)); + } + + #[tokio::test] + async fn test_multi_domain_with_peer_manager() { + let peer_mgr_a = create_mock_peer_manager().await; + let peer_mgr_b = create_mock_peer_manager().await; + connect_peer_manager(peer_mgr_a.clone(), peer_mgr_b.clone()).await; + wait_route_appear(peer_mgr_a.clone(), peer_mgr_b.clone()) + .await + .unwrap(); + + assert_eq!(peer_mgr_a.get_peer_map().list_peers().await.len(), 1); + assert_eq!( + peer_mgr_a.get_peer_map().list_peers().await[0], + peer_mgr_b.my_peer_id() + ); + + register_service(&peer_mgr_b.get_peer_rpc_mgr(), "test1", 0, "Hello"); + register_service(&peer_mgr_b.get_peer_rpc_mgr(), "test2", 20000, "Hello2"); + + let stub1 = peer_mgr_a + .get_peer_rpc_mgr() + .rpc_client() + .scoped_client::>( + peer_mgr_a.my_peer_id(), + peer_mgr_b.my_peer_id(), + "test1".to_string(), + ); + + let stub2 = peer_mgr_a + .get_peer_rpc_mgr() + .rpc_client() + .scoped_client::>( + peer_mgr_a.my_peer_id(), + peer_mgr_b.my_peer_id(), + "test2".to_string(), + ); + + let msg = random_string(16 * 1024); + let ret = stub1 + .say_hello( + RpcController::default(), + SayHelloRequest { name: msg.clone() }, + ) + .await + .unwrap(); + assert_eq!(ret.greeting, format!("Hello {}!", msg)); + + let ret = stub2 + .say_hello( + RpcController::default(), + SayHelloRequest { name: msg.clone() }, + ) + .await; + assert!(ret.is_err() && ret.unwrap_err().to_string().contains("Timeout")); + } +} diff --git a/rust/easytier/src/peers/peer_rpc_service.rs b/rust/easytier/src/peers/peer_rpc_service.rs new file mode 100644 index 0000000..35c4fc7 --- /dev/null +++ b/rust/easytier/src/peers/peer_rpc_service.rs @@ -0,0 +1,41 @@ +use crate::{ + common::global_ctx::ArcGlobalCtx, + proto::{ + peer_rpc::{DirectConnectorRpc, GetIpListRequest, GetIpListResponse}, + rpc_types::{self, controller::BaseController}, + }, +}; + +#[derive(Clone)] +pub struct DirectConnectorManagerRpcServer { + // TODO: this only cache for one src peer, should make it global + global_ctx: ArcGlobalCtx, +} + +#[async_trait::async_trait] +impl DirectConnectorRpc for DirectConnectorManagerRpcServer { + type Controller = BaseController; + + async fn get_ip_list( + &self, + _: BaseController, + _: GetIpListRequest, + ) -> rpc_types::error::Result { + let mut ret = self.global_ctx.get_ip_collector().collect_ip_addrs().await; + ret.listeners = self + .global_ctx + .config + .get_mapped_listeners() + .into_iter() + .chain(self.global_ctx.get_running_listeners().into_iter()) + .map(Into::into) + .collect(); + Ok(ret) + } +} + +impl DirectConnectorManagerRpcServer { + pub fn new(global_ctx: ArcGlobalCtx) -> Self { + Self { global_ctx } + } +} diff --git a/rust/easytier/src/peers/peer_task.rs b/rust/easytier/src/peers/peer_task.rs new file mode 100644 index 0000000..aa172b1 --- /dev/null +++ b/rust/easytier/src/peers/peer_task.rs @@ -0,0 +1,131 @@ +use std::result::Result; +use std::sync::{Arc, Mutex}; + +use async_trait::async_trait; +use dashmap::DashMap; +use tokio::select; +use tokio::sync::Notify; +use tokio::task::JoinHandle; + +use crate::common::scoped_task::ScopedTask; +use anyhow::Error; + +use super::peer_manager::PeerManager; + +#[async_trait] +pub trait PeerTaskLauncher: Send + Sync + Clone + 'static { + type Data; + type CollectPeerItem; + type TaskRet; + + fn new_data(&self, peer_mgr: Arc) -> Self::Data; + async fn collect_peers_need_task(&self, data: &Self::Data) -> Vec; + async fn launch_task( + &self, + data: &Self::Data, + item: Self::CollectPeerItem, + ) -> JoinHandle>; + + async fn all_task_done(&self, _data: &Self::Data) {} + + fn loop_interval_ms(&self) -> u64 { + 5000 + } +} + +pub struct PeerTaskManager { + launcher: Launcher, + peer_mgr: Arc, + main_loop_task: Mutex>>, + run_signal: Arc, + data: Launcher::Data, +} + +impl PeerTaskManager +where + D: Send + Sync + Clone + 'static, + C: std::fmt::Debug + Send + Sync + Clone + core::hash::Hash + Eq + 'static, + T: Send + 'static, + L: PeerTaskLauncher + 'static, +{ + pub fn new(launcher: L, peer_mgr: Arc) -> Self { + let data = launcher.new_data(peer_mgr.clone()); + Self { + launcher, + peer_mgr, + main_loop_task: Mutex::new(None), + run_signal: Arc::new(Notify::new()), + data, + } + } + + pub fn start(&self) { + let task = tokio::spawn(Self::main_loop( + self.launcher.clone(), + self.data.clone(), + self.run_signal.clone(), + )) + .into(); + self.main_loop_task.lock().unwrap().replace(task); + } + + async fn main_loop(launcher: L, data: D, signal: Arc) { + let peer_task_map = Arc::new(DashMap::>>::new()); + + loop { + let peers_to_connect = launcher.collect_peers_need_task(&data).await; + + // remove task not in peers_to_connect + let mut to_remove = vec![]; + for item in peer_task_map.iter() { + if !peers_to_connect.contains(item.key()) || item.value().is_finished() { + to_remove.push(item.key().clone()); + } + } + + for key in to_remove { + if let Some((_, task)) = peer_task_map.remove(&key) { + task.abort(); + match task.await { + Ok(Ok(_)) => {} + Ok(Err(task_ret)) => { + tracing::error!(?task_ret, "hole punching task failed"); + } + Err(e) => { + tracing::error!(?e, "hole punching task aborted"); + } + } + } + } + + if !peers_to_connect.is_empty() { + for item in peers_to_connect { + if peer_task_map.contains_key(&item) { + continue; + } + + tracing::debug!(?item, "launch hole punching task"); + peer_task_map + .insert(item.clone(), launcher.launch_task(&data, item).await.into()); + } + } else if peer_task_map.is_empty() { + launcher.all_task_done(&data).await; + } + + select! { + _ = tokio::time::sleep(std::time::Duration::from_millis( + launcher.loop_interval_ms(), + )) => {}, + _ = signal.notified() => {} + } + } + } + + pub async fn run_immediately(&self) { + self.run_signal.notify_one(); + } + + pub fn data(&self) -> D { + self.data.clone() + } +} diff --git a/rust/easytier/src/peers/route_trait.rs b/rust/easytier/src/peers/route_trait.rs new file mode 100644 index 0000000..76fd1fb --- /dev/null +++ b/rust/easytier/src/peers/route_trait.rs @@ -0,0 +1,107 @@ +use std::{net::Ipv4Addr, sync::Arc}; + +use dashmap::DashMap; + +use crate::{ + common::{global_ctx::NetworkIdentity, PeerId}, + proto::{ + common::PeerFeatureFlag, + peer_rpc::{ + ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, RouteForeignNetworkInfos, + }, + }, +}; + +#[derive(Clone, Debug)] +pub enum NextHopPolicy { + LeastHop, + LeastCost, +} + +impl Default for NextHopPolicy { + fn default() -> Self { + NextHopPolicy::LeastHop + } +} + +pub type ForeignNetworkRouteInfoMap = + DashMap; + +#[async_trait::async_trait] +pub trait RouteInterface { + async fn list_peers(&self) -> Vec; + fn my_peer_id(&self) -> PeerId; + async fn list_foreign_networks(&self) -> ForeignNetworkRouteInfoMap { + DashMap::new() + } +} + +pub type RouteInterfaceBox = Box; + +#[auto_impl::auto_impl(Box , &mut)] +pub trait RouteCostCalculatorInterface: Send + Sync { + fn begin_update(&mut self) {} + fn end_update(&mut self) {} + + fn calculate_cost(&self, _src: PeerId, _dst: PeerId) -> i32 { + 1 + } + + fn need_update(&self) -> bool { + false + } + + fn dump(&self) -> String { + "All routes have cost 1".to_string() + } +} + +#[derive(Clone, Debug, Default)] +pub struct DefaultRouteCostCalculator; + +impl RouteCostCalculatorInterface for DefaultRouteCostCalculator {} + +pub type RouteCostCalculator = Box; + +#[async_trait::async_trait] +#[auto_impl::auto_impl(Box, Arc)] +pub trait Route { + async fn open(&self, interface: RouteInterfaceBox) -> Result; + async fn close(&self); + + async fn get_next_hop(&self, peer_id: PeerId) -> Option; + async fn get_next_hop_with_policy( + &self, + peer_id: PeerId, + _policy: NextHopPolicy, + ) -> Option { + self.get_next_hop(peer_id).await + } + + async fn list_routes(&self) -> Vec; + + async fn get_peer_id_by_ipv4(&self, _ipv4: &Ipv4Addr) -> Option { + None + } + + async fn list_peers_own_foreign_network( + &self, + _network_identity: &NetworkIdentity, + ) -> Vec { + vec![] + } + + async fn list_foreign_network_info(&self) -> RouteForeignNetworkInfos { + Default::default() + } + + async fn set_route_cost_fn(&self, _cost_fn: RouteCostCalculator) {} + + async fn get_feature_flag(&self, peer_id: PeerId) -> Option; + + async fn dump(&self) -> String { + "this route implementation does not support dump".to_string() + } +} + +pub type ArcRoute = Arc>; diff --git a/rust/easytier/src/peers/rpc_service.rs b/rust/easytier/src/peers/rpc_service.rs new file mode 100644 index 0000000..34dfac0 --- /dev/null +++ b/rust/easytier/src/peers/rpc_service.rs @@ -0,0 +1,127 @@ +use std::sync::Arc; + +use crate::proto::{ + cli::{ + DumpRouteRequest, DumpRouteResponse, ListForeignNetworkRequest, ListForeignNetworkResponse, + ListGlobalForeignNetworkRequest, ListGlobalForeignNetworkResponse, ListPeerRequest, + ListPeerResponse, ListRouteRequest, ListRouteResponse, PeerInfo, PeerManageRpc, + ShowNodeInfoRequest, ShowNodeInfoResponse, + }, + rpc_types::{self, controller::BaseController}, +}; + +use super::peer_manager::PeerManager; + +#[derive(Clone)] +pub struct PeerManagerRpcService { + peer_manager: Arc, +} + +impl PeerManagerRpcService { + pub fn new(peer_manager: Arc) -> Self { + PeerManagerRpcService { peer_manager } + } + + pub async fn list_peers(&self) -> Vec { + let mut peers = self.peer_manager.get_peer_map().list_peers().await; + peers.extend( + self.peer_manager + .get_foreign_network_client() + .get_peer_map() + .list_peers() + .await + .iter(), + ); + let mut peer_infos = Vec::new(); + for peer in peers { + let mut peer_info = PeerInfo::default(); + peer_info.peer_id = peer; + + if let Some(conns) = self.peer_manager.get_peer_map().list_peer_conns(peer).await { + peer_info.conns = conns; + } else if let Some(conns) = self + .peer_manager + .get_foreign_network_client() + .get_peer_map() + .list_peer_conns(peer) + .await + { + peer_info.conns = conns; + } + + peer_infos.push(peer_info); + } + + peer_infos + } +} + +#[async_trait::async_trait] +impl PeerManageRpc for PeerManagerRpcService { + type Controller = BaseController; + async fn list_peer( + &self, + _: BaseController, + _request: ListPeerRequest, // Accept request of type HelloRequest + ) -> Result { + let mut reply = ListPeerResponse::default(); + + let peers = self.list_peers().await; + for peer in peers { + reply.peer_infos.push(peer); + } + + Ok(reply) + } + + async fn list_route( + &self, + _: BaseController, + _request: ListRouteRequest, // Accept request of type HelloRequest + ) -> Result { + let mut reply = ListRouteResponse::default(); + reply.routes = self.peer_manager.list_routes().await; + Ok(reply) + } + + async fn dump_route( + &self, + _: BaseController, + _request: DumpRouteRequest, // Accept request of type HelloRequest + ) -> Result { + let mut reply = DumpRouteResponse::default(); + reply.result = self.peer_manager.dump_route().await; + Ok(reply) + } + + async fn list_foreign_network( + &self, + _: BaseController, + _request: ListForeignNetworkRequest, // Accept request of type HelloRequest + ) -> Result { + let reply = self + .peer_manager + .get_foreign_network_manager() + .list_foreign_networks() + .await; + Ok(reply) + } + + async fn list_global_foreign_network( + &self, + _: BaseController, + _request: ListGlobalForeignNetworkRequest, + ) -> Result { + Ok(self.peer_manager.list_global_foreign_network().await) + } + + async fn show_node_info( + &self, + _: BaseController, + _request: ShowNodeInfoRequest, // Accept request of type HelloRequest + ) -> Result { + Ok(ShowNodeInfoResponse { + node_info: Some(self.peer_manager.get_my_info()), + }) + } +} diff --git a/rust/easytier/src/peers/tests.rs b/rust/easytier/src/peers/tests.rs new file mode 100644 index 0000000..0b1d971 --- /dev/null +++ b/rust/easytier/src/peers/tests.rs @@ -0,0 +1,129 @@ +use std::sync::Arc; + +use crate::{ + common::{ + error::Error, + global_ctx::{ + tests::{get_mock_global_ctx, get_mock_global_ctx_with_network}, + NetworkIdentity, + }, + PeerId, + }, + tunnel::ring::create_ring_tunnel_pair, +}; + +use super::{ + create_packet_recv_chan, + peer_manager::{PeerManager, RouteAlgoType}, +}; + +pub async fn create_mock_peer_manager() -> Arc { + let (s, _r) = create_packet_recv_chan(); + let peer_mgr = Arc::new(PeerManager::new( + RouteAlgoType::Ospf, + get_mock_global_ctx(), + s, + )); + peer_mgr.run().await.unwrap(); + peer_mgr +} + +pub async fn create_mock_peer_manager_with_name(network_name: String) -> Arc { + let (s, _r) = create_packet_recv_chan(); + let g = + get_mock_global_ctx_with_network(Some(NetworkIdentity::new(network_name, "".to_string()))); + let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, g, s)); + peer_mgr.run().await.unwrap(); + peer_mgr +} + +pub async fn connect_peer_manager(client: Arc, server: Arc) { + let (a_ring, b_ring) = create_ring_tunnel_pair(); + let a_mgr_copy = client.clone(); + tokio::spawn(async move { + a_mgr_copy.add_client_tunnel(a_ring).await.unwrap(); + }); + let b_mgr_copy = server.clone(); + tokio::spawn(async move { + b_mgr_copy.add_tunnel_as_server(b_ring).await.unwrap(); + }); +} + +pub async fn wait_route_appear_with_cost( + peer_mgr: Arc, + node_id: PeerId, + cost: Option, +) -> Result<(), Error> { + let now = std::time::Instant::now(); + while now.elapsed().as_secs() < 5 { + let route = peer_mgr.list_routes().await; + if route + .iter() + .any(|r| r.peer_id == node_id && (cost.is_none() || r.cost == cost.unwrap())) + { + return Ok(()); + } + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + return Err(Error::NotFound); +} + +pub async fn wait_route_appear( + peer_mgr: Arc, + target_peer: Arc, +) -> Result<(), Error> { + wait_route_appear_with_cost(peer_mgr.clone(), target_peer.my_peer_id(), None).await?; + wait_route_appear_with_cost(target_peer, peer_mgr.my_peer_id(), None).await +} + +#[tokio::test] +async fn foreign_mgr_stress_test() { + const FOREIGN_NETWORK_COUNT: i32 = 20; + const PEER_PER_NETWORK: i32 = 3; + const PUBLIC_PEER_COUNT: i32 = 3; + + let mut public_peers = Vec::new(); + for _ in 0..PUBLIC_PEER_COUNT { + public_peers.push(create_mock_peer_manager().await); + } + connect_peer_manager(public_peers[0].clone(), public_peers[1].clone()).await; + connect_peer_manager(public_peers[0].clone(), public_peers[2].clone()).await; + connect_peer_manager(public_peers[1].clone(), public_peers[2].clone()).await; + + let mut foreigns = Vec::new(); + + for i in 0..FOREIGN_NETWORK_COUNT { + let mut peers = Vec::new(); + + let name = format!("foreign-network-test-{}", i); + + for _ in 0..PEER_PER_NETWORK { + let mgr = create_mock_peer_manager_with_name(name.clone()).await; + let public_peer_idx = rand::random::() % public_peers.len(); + connect_peer_manager(mgr.clone(), public_peers[public_peer_idx].clone()).await; + peers.push(mgr); + } + + foreigns.push(peers); + } + + for _ in 0..5 { + for i in 0..PUBLIC_PEER_COUNT { + let p = public_peers[i as usize].clone(); + println!( + "public peer {} routes: {:?}, global_foreign_network: {:?}, peers: {:?}", + i, + p.list_routes().await, + p.list_global_foreign_network().await.foreign_networks.len(), + p.get_peer_map().list_peers().await + ); + } + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let new_peer = create_mock_peer_manager().await; + connect_peer_manager(new_peer.clone(), public_peers[0].clone()).await; + while let Err(e) = wait_route_appear(public_peers[1].clone(), new_peer.clone()).await { + println!("wait route ret: {:?}", e); + } + } +} diff --git a/rust/easytier/src/proto/cli.proto b/rust/easytier/src/proto/cli.proto new file mode 100644 index 0000000..22096d2 --- /dev/null +++ b/rust/easytier/src/proto/cli.proto @@ -0,0 +1,219 @@ +syntax = "proto3"; + +import "common.proto"; + +package cli; + +message Status { + int32 code = 1; + string message = 2; +} + +message PeerConnStats { + uint64 rx_bytes = 1; + uint64 tx_bytes = 2; + + uint64 rx_packets = 3; + uint64 tx_packets = 4; + + uint64 latency_us = 5; +} + +message PeerConnInfo { + string conn_id = 1; + uint32 my_peer_id = 2; + uint32 peer_id = 3; + repeated string features = 4; + common.TunnelInfo tunnel = 5; + PeerConnStats stats = 6; + float loss_rate = 7; + bool is_client = 8; + string network_name = 9; +} + +message PeerInfo { + uint32 peer_id = 1; + repeated PeerConnInfo conns = 2; +} + +message ListPeerRequest {} + +message ListPeerResponse { + repeated PeerInfo peer_infos = 1; + NodeInfo my_info = 2; +} + +message Route { + uint32 peer_id = 1; + common.Ipv4Inet ipv4_addr = 2; + + uint32 next_hop_peer_id = 3; + int32 cost = 4; + int32 path_latency = 11; + + repeated string proxy_cidrs = 5; + string hostname = 6; + common.StunInfo stun_info = 7; + string inst_id = 8; + string version = 9; + common.PeerFeatureFlag feature_flag = 10; + + optional uint32 next_hop_peer_id_latency_first = 12; + optional int32 cost_latency_first = 13; + optional int32 path_latency_latency_first = 14; +} + +message PeerRoutePair { + Route route = 1; + PeerInfo peer = 2; +} + +message NodeInfo { + uint32 peer_id = 1; + string ipv4_addr = 2; + repeated string proxy_cidrs = 3; + string hostname = 4; + common.StunInfo stun_info = 5; + string inst_id = 6; + repeated string listeners = 7; + string config = 8; + string version = 9; + common.PeerFeatureFlag feature_flag = 10; +} + +message ShowNodeInfoRequest {} + +message ShowNodeInfoResponse { NodeInfo node_info = 1; } + +message ListRouteRequest {} + +message ListRouteResponse { repeated Route routes = 1; } + +message DumpRouteRequest {} + +message DumpRouteResponse { string result = 1; } + +message ListForeignNetworkRequest {} + +message ForeignNetworkEntryPb { + repeated PeerInfo peers = 1; + bytes network_secret_digest = 2; +} + +message ListForeignNetworkResponse { + // foreign network in local + map foreign_networks = 1; +} + +message ListGlobalForeignNetworkRequest {} + +message ListGlobalForeignNetworkResponse { + // foreign network in the entire network + message OneForeignNetwork { + string network_name = 1; + repeated uint32 peer_ids = 2; + string last_updated = 3; + uint32 version = 4; + } + + message ForeignNetworks { repeated OneForeignNetwork foreign_networks = 1; } + + map foreign_networks = 1; +} + +service PeerManageRpc { + rpc ListPeer(ListPeerRequest) returns (ListPeerResponse); + rpc ListRoute(ListRouteRequest) returns (ListRouteResponse); + rpc DumpRoute(DumpRouteRequest) returns (DumpRouteResponse); + rpc ListForeignNetwork(ListForeignNetworkRequest) + returns (ListForeignNetworkResponse); + rpc ListGlobalForeignNetwork(ListGlobalForeignNetworkRequest) + returns (ListGlobalForeignNetworkResponse); + rpc ShowNodeInfo(ShowNodeInfoRequest) returns (ShowNodeInfoResponse); +} + +enum ConnectorStatus { + CONNECTED = 0; + DISCONNECTED = 1; + CONNECTING = 2; +} + +message Connector { + common.Url url = 1; + ConnectorStatus status = 2; +} + +message ListConnectorRequest {} + +message ListConnectorResponse { repeated Connector connectors = 1; } + +enum ConnectorManageAction { + ADD = 0; + REMOVE = 1; +} + +message ManageConnectorRequest { + ConnectorManageAction action = 1; + common.Url url = 2; +} + +message ManageConnectorResponse {} + +service ConnectorManageRpc { + rpc ListConnector(ListConnectorRequest) returns (ListConnectorResponse); + rpc ManageConnector(ManageConnectorRequest) returns (ManageConnectorResponse); +} + +message VpnPortalInfo { + string vpn_type = 1; + string client_config = 2; + repeated string connected_clients = 3; +} + +message GetVpnPortalInfoRequest {} +message GetVpnPortalInfoResponse { VpnPortalInfo vpn_portal_info = 1; } + +service VpnPortalRpc { + rpc GetVpnPortalInfo(GetVpnPortalInfoRequest) + returns (GetVpnPortalInfoResponse); +} + +enum TcpProxyEntryTransportType { + TCP = 0; + KCP = 1; +} + +enum TcpProxyEntryState { + Unknown = 0; + // receive syn packet but not start connecting to dst + SynReceived = 1; + // connecting to dst + ConnectingDst = 2; + // connected to dst + Connected = 3; + // connection closed + Closed = 4; + // closing src + ClosingSrc = 5; + // closing dst + ClosingDst = 6; +} + +message TcpProxyEntry { + common.SocketAddr src = 1; + common.SocketAddr dst = 2; + uint64 start_time = 3; + TcpProxyEntryState state = 4; + TcpProxyEntryTransportType transport_type = 5; +} + +message ListTcpProxyEntryRequest {} + +message ListTcpProxyEntryResponse { + repeated TcpProxyEntry entries = 1; +} + +service TcpProxyRpc { + rpc ListTcpProxyEntry(ListTcpProxyEntryRequest) + returns (ListTcpProxyEntryResponse); +} diff --git a/rust/easytier/src/proto/cli.rs b/rust/easytier/src/proto/cli.rs new file mode 100644 index 0000000..236df89 --- /dev/null +++ b/rust/easytier/src/proto/cli.rs @@ -0,0 +1,113 @@ +include!(concat!(env!("OUT_DIR"), "/cli.rs")); + +impl PeerRoutePair { + pub fn get_latency_ms(&self) -> Option { + let mut ret = u64::MAX; + let p = self.peer.as_ref()?; + for conn in p.conns.iter() { + let Some(stats) = &conn.stats else { + continue; + }; + ret = ret.min(stats.latency_us); + } + + if ret == u64::MAX { + None + } else { + Some(f64::from(ret as u32) / 1000.0) + } + } + + pub fn get_rx_bytes(&self) -> Option { + let mut ret = 0; + let p = self.peer.as_ref()?; + for conn in p.conns.iter() { + let Some(stats) = &conn.stats else { + continue; + }; + ret += stats.rx_bytes; + } + + if ret == 0 { + None + } else { + Some(ret) + } + } + + pub fn get_tx_bytes(&self) -> Option { + let mut ret = 0; + let p = self.peer.as_ref()?; + for conn in p.conns.iter() { + let Some(stats) = &conn.stats else { + continue; + }; + ret += stats.tx_bytes; + } + + if ret == 0 { + None + } else { + Some(ret) + } + } + + pub fn get_loss_rate(&self) -> Option { + let mut ret = 0.0; + let p = self.peer.as_ref()?; + for conn in p.conns.iter() { + ret += conn.loss_rate; + } + + if ret == 0.0 { + None + } else { + Some(ret as f64) + } + } + + pub fn get_conn_protos(&self) -> Option> { + let mut ret = vec![]; + let p = self.peer.as_ref()?; + for conn in p.conns.iter() { + let Some(tunnel_info) = &conn.tunnel else { + continue; + }; + // insert if not exists + if !ret.contains(&tunnel_info.tunnel_type) { + ret.push(tunnel_info.tunnel_type.clone()); + } + } + + if ret.is_empty() { + None + } else { + Some(ret) + } + } + + pub fn get_udp_nat_type(self: &Self) -> String { + use crate::proto::common::NatType; + let mut ret = NatType::Unknown; + if let Some(r) = &self.route.clone().unwrap_or_default().stun_info { + ret = NatType::try_from(r.udp_nat_type).unwrap(); + } + format!("{:?}", ret) + } +} + +pub fn list_peer_route_pair(peers: Vec, routes: Vec) -> Vec { + let mut pairs: Vec = vec![]; + + for route in routes.iter() { + let peer = peers.iter().find(|peer| peer.peer_id == route.peer_id); + let pair = PeerRoutePair { + route: Some(route.clone()), + peer: peer.cloned(), + }; + + pairs.push(pair); + } + + pairs +} diff --git a/rust/easytier/src/proto/common.proto b/rust/easytier/src/proto/common.proto new file mode 100644 index 0000000..7519860 --- /dev/null +++ b/rust/easytier/src/proto/common.proto @@ -0,0 +1,157 @@ +syntax = "proto3"; + +import "error.proto"; + +package common; + +message FlagsInConfig { + string default_protocol = 1; + string dev_name = 2; + bool enable_encryption = 3; + bool enable_ipv6 = 4; + uint32 mtu = 5; + bool latency_first = 6; + bool enable_exit_node = 7; + bool no_tun = 8; + bool use_smoltcp = 9; + string relay_network_whitelist = 10; + bool disable_p2p = 11; + bool relay_all_peer_rpc = 12; + bool disable_udp_hole_punching = 13; + string ipv6_listener = 14; + bool multi_thread = 15; + CompressionAlgoPb data_compress_algo = 16; + bool bind_device = 17; + + // should we convert all tcp streams into kcp streams + bool enable_kcp_proxy = 18; + // does this peer allow kcp input + bool disable_kcp_input = 19; + // allow relay kcp packets (for public server, this can reduce the throughput) + bool disable_relay_kcp = 20; + bool proxy_forward_by_system = 21; +} + +message RpcDescriptor { + // allow same service registered multiple times in different domain + string domain_name = 1; + + string proto_name = 2; + string service_name = 3; + uint32 method_index = 4; +} + +message RpcRequest { + RpcDescriptor descriptor = 1 [ deprecated = true ]; + + bytes request = 2; + int32 timeout_ms = 3; +} + +message RpcResponse { + bytes response = 1; + error.Error error = 2; + + uint64 runtime_us = 3; +} + +enum CompressionAlgoPb { + Invalid = 0; + None = 1; + Zstd = 2; +} + +message RpcCompressionInfo { + // use this to compress the content + CompressionAlgoPb algo = 1; + + // tell the peer which compression algo is used to compress the next + // response/request + CompressionAlgoPb accepted_algo = 2; +} + +message RpcPacket { + uint32 from_peer = 1; + uint32 to_peer = 2; + int64 transaction_id = 3; + + RpcDescriptor descriptor = 4; + bytes body = 5; + bool is_request = 6; + + uint32 total_pieces = 7; + uint32 piece_idx = 8; + + int32 trace_id = 9; + + RpcCompressionInfo compression_info = 10; +} + +message Void {} + +message UUID { + uint32 part1 = 1; + uint32 part2 = 2; + uint32 part3 = 3; + uint32 part4 = 4; +} + +enum NatType { + // has NAT; but own a single public IP, port is not changed + Unknown = 0; + OpenInternet = 1; + NoPAT = 2; + FullCone = 3; + Restricted = 4; + PortRestricted = 5; + Symmetric = 6; + SymUdpFirewall = 7; + SymmetricEasyInc = 8; + SymmetricEasyDec = 9; +} + +message Ipv4Addr { uint32 addr = 1; } + +message Ipv6Addr { + uint32 part1 = 1; + uint32 part2 = 2; + uint32 part3 = 3; + uint32 part4 = 4; +} + +message Ipv4Inet { + Ipv4Addr address = 1; + uint32 network_length = 2; +} + +message Url { string url = 1; } + +message SocketAddr { + oneof ip { + Ipv4Addr ipv4 = 1; + Ipv6Addr ipv6 = 2; + }; + uint32 port = 3; +} + +message TunnelInfo { + string tunnel_type = 1; + common.Url local_addr = 2; + common.Url remote_addr = 3; +} + +message StunInfo { + NatType udp_nat_type = 1; + NatType tcp_nat_type = 2; + int64 last_update_time = 3; + repeated string public_ip = 4; + uint32 min_port = 5; + uint32 max_port = 6; +} + +message PeerFeatureFlag { + bool is_public_server = 1; + bool avoid_relay_data = 2; + bool kcp_input = 3; + bool no_relay_kcp = 4; +} diff --git a/rust/easytier/src/proto/common.rs b/rust/easytier/src/proto/common.rs new file mode 100644 index 0000000..a40e471 --- /dev/null +++ b/rust/easytier/src/proto/common.rs @@ -0,0 +1,214 @@ +use std::{fmt::Display, str::FromStr}; + +use anyhow::Context; + +use crate::tunnel::packet_def::CompressorAlgo; + +include!(concat!(env!("OUT_DIR"), "/common.rs")); + +impl From for Uuid { + fn from(uuid: uuid::Uuid) -> Self { + let (high, low) = uuid.as_u64_pair(); + Uuid { + part1: (high >> 32) as u32, + part2: (high & 0xFFFFFFFF) as u32, + part3: (low >> 32) as u32, + part4: (low & 0xFFFFFFFF) as u32, + } + } +} + +impl From for uuid::Uuid { + fn from(uuid: Uuid) -> Self { + uuid::Uuid::from_u64_pair( + (u64::from(uuid.part1) << 32) | u64::from(uuid.part2), + (u64::from(uuid.part3) << 32) | u64::from(uuid.part4), + ) + } +} + +impl From for Uuid { + fn from(value: String) -> Self { + uuid::Uuid::parse_str(&value).unwrap().into() + } +} + +impl Display for Uuid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", uuid::Uuid::from(self.clone())) + } +} + +impl From for Ipv4Addr { + fn from(value: std::net::Ipv4Addr) -> Self { + Self { + addr: u32::from_be_bytes(value.octets()), + } + } +} + +impl From for std::net::Ipv4Addr { + fn from(value: Ipv4Addr) -> Self { + std::net::Ipv4Addr::from(value.addr) + } +} + +impl ToString for Ipv4Addr { + fn to_string(&self) -> String { + std::net::Ipv4Addr::from(self.addr).to_string() + } +} + +impl From for Ipv6Addr { + fn from(value: std::net::Ipv6Addr) -> Self { + let b = value.octets(); + Self { + part1: u32::from_be_bytes([b[0], b[1], b[2], b[3]]), + part2: u32::from_be_bytes([b[4], b[5], b[6], b[7]]), + part3: u32::from_be_bytes([b[8], b[9], b[10], b[11]]), + part4: u32::from_be_bytes([b[12], b[13], b[14], b[15]]), + } + } +} + +impl From for std::net::Ipv6Addr { + fn from(value: Ipv6Addr) -> Self { + let part1 = value.part1.to_be_bytes(); + let part2 = value.part2.to_be_bytes(); + let part3 = value.part3.to_be_bytes(); + let part4 = value.part4.to_be_bytes(); + std::net::Ipv6Addr::from([ + part1[0], part1[1], part1[2], part1[3], part2[0], part2[1], part2[2], part2[3], + part3[0], part3[1], part3[2], part3[3], part4[0], part4[1], part4[2], part4[3], + ]) + } +} + +impl ToString for Ipv6Addr { + fn to_string(&self) -> String { + std::net::Ipv6Addr::from(self.clone()).to_string() + } +} + +impl From for Ipv4Inet { + fn from(value: cidr::Ipv4Inet) -> Self { + Ipv4Inet { + address: Some(value.address().into()), + network_length: value.network_length() as u32, + } + } +} + +impl From for cidr::Ipv4Inet { + fn from(value: Ipv4Inet) -> Self { + cidr::Ipv4Inet::new( + value.address.unwrap_or_default().into(), + value.network_length as u8, + ) + .unwrap() + } +} + +impl std::fmt::Display for Ipv4Inet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", cidr::Ipv4Inet::from(self.clone())) + } +} + +impl FromStr for Ipv4Inet { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(Ipv4Inet::from( + cidr::Ipv4Inet::from_str(s).with_context(|| "Failed to parse Ipv4Inet")?, + )) + } +} + +impl From for Url { + fn from(value: url::Url) -> Self { + Url { + url: value.to_string(), + } + } +} + +impl From for url::Url { + fn from(value: Url) -> Self { + url::Url::parse(&value.url).unwrap() + } +} + +impl FromStr for Url { + type Err = url::ParseError; + + fn from_str(s: &str) -> Result { + Ok(Url { + url: s.parse::()?.to_string(), + }) + } +} + +impl Display for Url { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.url) + } +} + +impl From for SocketAddr { + fn from(value: std::net::SocketAddr) -> Self { + match value { + std::net::SocketAddr::V4(v4) => SocketAddr { + ip: Some(socket_addr::Ip::Ipv4(v4.ip().clone().into())), + port: v4.port() as u32, + }, + std::net::SocketAddr::V6(v6) => SocketAddr { + ip: Some(socket_addr::Ip::Ipv6(v6.ip().clone().into())), + port: v6.port() as u32, + }, + } + } +} + +impl From for std::net::SocketAddr { + fn from(value: SocketAddr) -> Self { + if value.ip.is_none() { + return "0.0.0.0:0".parse().unwrap(); + } + match value.ip.unwrap() { + socket_addr::Ip::Ipv4(ip) => std::net::SocketAddr::V4(std::net::SocketAddrV4::new( + std::net::Ipv4Addr::from(ip), + value.port as u16, + )), + socket_addr::Ip::Ipv6(ip) => std::net::SocketAddr::V6(std::net::SocketAddrV6::new( + std::net::Ipv6Addr::from(ip), + value.port as u16, + 0, + 0, + )), + } + } +} + +impl TryFrom for CompressorAlgo { + type Error = anyhow::Error; + + fn try_from(value: CompressionAlgoPb) -> Result { + match value { + CompressionAlgoPb::Zstd => Ok(CompressorAlgo::ZstdDefault), + CompressionAlgoPb::None => Ok(CompressorAlgo::None), + _ => Err(anyhow::anyhow!("Invalid CompressionAlgoPb")), + } + } +} + +impl TryFrom for CompressionAlgoPb { + type Error = anyhow::Error; + + fn try_from(value: CompressorAlgo) -> Result { + match value { + CompressorAlgo::ZstdDefault => Ok(CompressionAlgoPb::Zstd), + CompressorAlgo::None => Ok(CompressionAlgoPb::None), + } + } +} diff --git a/rust/easytier/src/proto/error.proto b/rust/easytier/src/proto/error.proto new file mode 100644 index 0000000..5b5f537 --- /dev/null +++ b/rust/easytier/src/proto/error.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; +package error; + +message OtherError { string error_message = 1; } + +message InvalidMethodIndex { + string service_name = 1; + uint32 method_index = 2; +} + +message InvalidService { string service_name = 1; } + +message ProstDecodeError {} + +message ProstEncodeError {} + +message ExecuteError { string error_message = 1; } + +message MalformatRpcPacket { string error_message = 1; } + +message Timeout { string error_message = 1; } + +message Error { + oneof error_kind { + OtherError other_error = 1; + InvalidMethodIndex invalid_method_index = 2; + InvalidService invalid_service = 3; + ProstDecodeError prost_decode_error = 4; + ProstEncodeError prost_encode_error = 5; + ExecuteError execute_error = 6; + MalformatRpcPacket malformat_rpc_packet = 7; + Timeout timeout = 8; + } +} diff --git a/rust/easytier/src/proto/error.rs b/rust/easytier/src/proto/error.rs new file mode 100644 index 0000000..b2cb2d3 --- /dev/null +++ b/rust/easytier/src/proto/error.rs @@ -0,0 +1,84 @@ +use prost::DecodeError; + +use super::rpc_types; + +include!(concat!(env!("OUT_DIR"), "/error.rs")); + +impl From<&rpc_types::error::Error> for Error { + fn from(e: &rpc_types::error::Error) -> Self { + use super::error::error::ErrorKind as ProtoError; + match e { + rpc_types::error::Error::ExecutionError(e) => Self { + error_kind: Some(ProtoError::ExecuteError(ExecuteError { + error_message: format!("{:?}", e), + })), + }, + rpc_types::error::Error::DecodeError(_) => Self { + error_kind: Some(ProtoError::ProstDecodeError(ProstDecodeError {})), + }, + rpc_types::error::Error::EncodeError(_) => Self { + error_kind: Some(ProtoError::ProstEncodeError(ProstEncodeError {})), + }, + rpc_types::error::Error::InvalidMethodIndex(m, s) => Self { + error_kind: Some(ProtoError::InvalidMethodIndex(InvalidMethodIndex { + method_index: *m as u32, + service_name: format!("{:?}", s), + })), + }, + rpc_types::error::Error::InvalidServiceKey(s, _) => Self { + error_kind: Some(ProtoError::InvalidService(InvalidService { + service_name: format!("{:?}", s), + })), + }, + rpc_types::error::Error::MalformatRpcPacket(e) => Self { + error_kind: Some(ProtoError::MalformatRpcPacket(MalformatRpcPacket { + error_message: format!("{:?}", e), + })), + }, + rpc_types::error::Error::Timeout(e) => Self { + error_kind: Some(ProtoError::Timeout(Timeout { + error_message: format!("{:?}", e), + })), + }, + #[allow(unreachable_patterns)] + e => Self { + error_kind: Some(ProtoError::OtherError(OtherError { + error_message: format!("{:?}", e), + })), + }, + } + } +} + +impl From<&Error> for rpc_types::error::Error { + fn from(e: &Error) -> Self { + use super::error::error::ErrorKind as ProtoError; + match &e.error_kind { + Some(ProtoError::ExecuteError(e)) => { + Self::ExecutionError(anyhow::anyhow!(e.error_message.clone())) + } + Some(ProtoError::ProstDecodeError(_)) => { + Self::DecodeError(DecodeError::new("decode error")) + } + Some(ProtoError::ProstEncodeError(_)) => { + Self::DecodeError(DecodeError::new("encode error")) + } + Some(ProtoError::InvalidMethodIndex(e)) => { + Self::InvalidMethodIndex(e.method_index as u8, e.service_name.clone()) + } + Some(ProtoError::InvalidService(e)) => { + Self::InvalidServiceKey(e.service_name.clone(), "".to_string()) + } + Some(ProtoError::MalformatRpcPacket(e)) => { + Self::MalformatRpcPacket(e.error_message.clone()) + } + Some(ProtoError::Timeout(e)) => { + Self::ExecutionError(anyhow::anyhow!(e.error_message.clone())) + } + Some(ProtoError::OtherError(e)) => { + Self::ExecutionError(anyhow::anyhow!(e.error_message.clone())) + } + None => Self::ExecutionError(anyhow::anyhow!("unknown error {:?}", e)), + } + } +} diff --git a/rust/easytier/src/proto/mod.rs b/rust/easytier/src/proto/mod.rs new file mode 100644 index 0000000..f1da8fd --- /dev/null +++ b/rust/easytier/src/proto/mod.rs @@ -0,0 +1,14 @@ +pub mod rpc_impl; +pub mod rpc_types; + +pub mod cli; +pub mod common; +pub mod error; +pub mod peer_rpc; +pub mod web; + +#[cfg(test)] +pub mod tests; + +const DESCRIPTOR_POOL_BYTES: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/file_descriptor_set.bin")); diff --git a/rust/easytier/src/proto/peer_rpc.proto b/rust/easytier/src/proto/peer_rpc.proto new file mode 100644 index 0000000..7f3e2a2 --- /dev/null +++ b/rust/easytier/src/proto/peer_rpc.proto @@ -0,0 +1,212 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; +import "common.proto"; + +package peer_rpc; + +message RoutePeerInfo { + // means next hop in route table. + uint32 peer_id = 1; + common.UUID inst_id = 2; + uint32 cost = 3; + optional common.Ipv4Addr ipv4_addr = 4; + repeated string proxy_cidrs = 5; + optional string hostname = 6; + common.NatType udp_stun_info = 7; + google.protobuf.Timestamp last_update = 8; + uint32 version = 9; + + string easytier_version = 10; + common.PeerFeatureFlag feature_flag = 11; + uint64 peer_route_id = 12; + + uint32 network_length = 13; +} + +message PeerIdVersion { + uint32 peer_id = 1; + uint32 version = 2; +} + +message RouteConnBitmap { + repeated PeerIdVersion peer_ids = 1; + bytes bitmap = 2; +} + +message RoutePeerInfos { repeated RoutePeerInfo items = 1; } + +message ForeignNetworkRouteInfoKey { + uint32 peer_id = 1; + string network_name = 2; +} + +message ForeignNetworkRouteInfoEntry { + repeated uint32 foreign_peer_ids = 1; + google.protobuf.Timestamp last_update = 2; + uint32 version = 3; + bytes network_secret_digest = 4; +} + +message RouteForeignNetworkInfos { + message Info { + ForeignNetworkRouteInfoKey key = 1; + ForeignNetworkRouteInfoEntry value = 2; + } + repeated Info infos = 1; +} + +message SyncRouteInfoRequest { + uint32 my_peer_id = 1; + uint64 my_session_id = 2; + bool is_initiator = 3; + RoutePeerInfos peer_infos = 4; + RouteConnBitmap conn_bitmap = 5; + RouteForeignNetworkInfos foreign_network_infos = 6; +} + +enum SyncRouteInfoError { + DuplicatePeerId = 0; + Stopped = 1; +} + +message SyncRouteInfoResponse { + bool is_initiator = 1; + uint64 session_id = 2; + optional SyncRouteInfoError error = 3; +} + +service OspfRouteRpc { + // Generates a "hello" greeting based on the supplied info. + rpc SyncRouteInfo(SyncRouteInfoRequest) returns (SyncRouteInfoResponse); +} + +message GetIpListRequest {} + +message GetIpListResponse { + common.Ipv4Addr public_ipv4 = 1; + repeated common.Ipv4Addr interface_ipv4s = 2; + common.Ipv6Addr public_ipv6 = 3; + repeated common.Ipv6Addr interface_ipv6s = 4; + repeated common.Url listeners = 5; +} + +service DirectConnectorRpc { + rpc GetIpList(GetIpListRequest) returns (GetIpListResponse); +} + +message SelectPunchListenerRequest { + bool force_new = 1; +} + +message SelectPunchListenerResponse { + common.SocketAddr listener_mapped_addr = 1; +} + +message SendPunchPacketConeRequest { + common.SocketAddr listener_mapped_addr = 1; + common.SocketAddr dest_addr = 2; + uint32 transaction_id = 3; + // send this many packets in a batch + uint32 packet_count_per_batch = 4; + // send total this batch count, total packet count = packet_batch_size * packet_batch_count + uint32 packet_batch_count = 5; + // interval between each batch + uint32 packet_interval_ms = 6; +} + +message SendPunchPacketHardSymRequest { + common.SocketAddr listener_mapped_addr = 1; + + repeated common.Ipv4Addr public_ips = 2; + uint32 transaction_id = 3; + uint32 port_index = 4; + uint32 round = 5; +} + +message SendPunchPacketHardSymResponse { uint32 next_port_index = 1; } + +message SendPunchPacketEasySymRequest { + common.SocketAddr listener_mapped_addr = 1; + repeated common.Ipv4Addr public_ips = 2; + uint32 transaction_id = 3; + + uint32 base_port_num = 4; + uint32 max_port_num = 5; + bool is_incremental = 6; +} + +message SendPunchPacketBothEasySymRequest { + uint32 udp_socket_count = 1; + common.Ipv4Addr public_ip = 2; + uint32 transaction_id = 3; + + uint32 dst_port_num = 4; + uint32 wait_time_ms = 5; +} + +message SendPunchPacketBothEasySymResponse { + // is doing punch with other peer + bool is_busy = 1; + common.SocketAddr base_mapped_addr = 2; +} + +service UdpHolePunchRpc { + rpc SelectPunchListener(SelectPunchListenerRequest) + returns (SelectPunchListenerResponse); + + // send packet to one remote_addr, used by nat1-3 to nat1-3 + rpc SendPunchPacketCone(SendPunchPacketConeRequest) returns (common.Void); + + // send packet to multiple remote_addr (birthday attack), used by nat4 to nat1-3 + rpc SendPunchPacketHardSym(SendPunchPacketHardSymRequest) + returns (SendPunchPacketHardSymResponse); + rpc SendPunchPacketEasySym(SendPunchPacketEasySymRequest) + returns (common.Void); + + // nat4 to nat4 (both predictably) + rpc SendPunchPacketBothEasySym(SendPunchPacketBothEasySymRequest) + returns (SendPunchPacketBothEasySymResponse); +} + +message DirectConnectedPeerInfo { int32 latency_ms = 1; } + +message PeerInfoForGlobalMap { + map direct_peers = 1; +} + +message ReportPeersRequest { + uint32 my_peer_id = 1; + PeerInfoForGlobalMap peer_infos = 2; +} + +message ReportPeersResponse {} + +message GlobalPeerMap { map map = 1; } + +message GetGlobalPeerMapRequest { uint64 digest = 1; } + +message GetGlobalPeerMapResponse { + map global_peer_map = 1; + optional uint64 digest = 2; +} + +service PeerCenterRpc { + rpc ReportPeers(ReportPeersRequest) returns (ReportPeersResponse); + rpc GetGlobalPeerMap(GetGlobalPeerMapRequest) + returns (GetGlobalPeerMapResponse); +} + +message HandshakeRequest { + uint32 magic = 1; + uint32 my_peer_id = 2; + uint32 version = 3; + repeated string features = 4; + string network_name = 5; + bytes network_secret_digrest = 6; +} + +message KcpConnData { + common.SocketAddr src = 1; + common.SocketAddr dst = 4; +} diff --git a/rust/easytier/src/proto/peer_rpc.rs b/rust/easytier/src/proto/peer_rpc.rs new file mode 100644 index 0000000..8b2b432 --- /dev/null +++ b/rust/easytier/src/proto/peer_rpc.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/peer_rpc.rs")); diff --git a/rust/easytier/src/proto/rpc_impl/bidirect.rs b/rust/easytier/src/proto/rpc_impl/bidirect.rs new file mode 100644 index 0000000..a673949 --- /dev/null +++ b/rust/easytier/src/proto/rpc_impl/bidirect.rs @@ -0,0 +1,181 @@ +use std::sync::{atomic::AtomicBool, Arc, Mutex}; + +use futures::{SinkExt as _, StreamExt}; +use tokio::{task::JoinSet, time::timeout}; + +use crate::{ + defer, + proto::rpc_types::error::Error, + tunnel::{packet_def::PacketType, ring::create_ring_tunnel_pair, Tunnel}, +}; + +use super::{client::Client, server::Server}; + +pub struct BidirectRpcManager { + rpc_client: Client, + rpc_server: Server, + + rx_timeout: Option, + error: Arc>>, + tunnel: Mutex>>, + running: Arc, + + tasks: Mutex>>, +} + +impl BidirectRpcManager { + pub fn new() -> Self { + Self { + rpc_client: Client::new(), + rpc_server: Server::new(), + + rx_timeout: None, + error: Arc::new(Mutex::new(None)), + tunnel: Mutex::new(None), + running: Arc::new(AtomicBool::new(false)), + + tasks: Mutex::new(None), + } + } + + pub fn set_rx_timeout(mut self, timeout: Option) -> Self { + self.rx_timeout = timeout; + self + } + + pub fn run_and_create_tunnel(&self) -> Box { + let (ret, inner) = create_ring_tunnel_pair(); + self.run_with_tunnel(inner); + ret + } + + pub fn run_with_tunnel(&self, inner: Box) { + let mut tasks = JoinSet::new(); + self.rpc_client.run(); + self.rpc_server.run(); + self.running + .store(true, std::sync::atomic::Ordering::Relaxed); + + let (server_tx, mut server_rx) = ( + self.rpc_server.get_transport_sink(), + self.rpc_server.get_transport_stream(), + ); + let (client_tx, mut client_rx) = ( + self.rpc_client.get_transport_sink(), + self.rpc_client.get_transport_stream(), + ); + + let (mut inner_rx, mut inner_tx) = inner.split(); + self.tunnel.lock().unwrap().replace(inner); + + let e_clone = self.error.clone(); + let r_clone = self.running.clone(); + tasks.spawn(async move { + defer! { + r_clone.store(false, std::sync::atomic::Ordering::Relaxed); + } + loop { + let packet = tokio::select! { + Some(Ok(packet)) = server_rx.next() => { + tracing::trace!(?packet, "recv rpc packet from server"); + packet + } + Some(Ok(packet)) = client_rx.next() => { + tracing::trace!(?packet, "recv rpc packet from client"); + packet + } + else => { + tracing::warn!("rpc transport read aborted, exiting"); + break; + } + }; + + if let Err(e) = inner_tx.send(packet).await { + tracing::error!(error = ?e, "send to peer failed"); + e_clone.lock().unwrap().replace(Error::from(e)); + } + } + }); + + let recv_timeout = self.rx_timeout; + let e_clone = self.error.clone(); + let r_clone = self.running.clone(); + tasks.spawn(async move { + defer! { + r_clone.store(false, std::sync::atomic::Ordering::Relaxed); + } + loop { + let ret = if let Some(recv_timeout) = recv_timeout { + match timeout(recv_timeout, inner_rx.next()).await { + Ok(ret) => ret, + Err(e) => { + e_clone.lock().unwrap().replace(e.into()); + break; + } + } + } else { + inner_rx.next().await + }; + + let o = match ret { + Some(Ok(o)) => o, + Some(Err(e)) => { + tracing::error!(error = ?e, "recv from peer failed"); + e_clone.lock().unwrap().replace(Error::from(e)); + break; + } + None => { + tracing::warn!("peer rpc transport read aborted, exiting"); + e_clone.lock().unwrap().replace(Error::Shutdown); + break; + } + }; + + if o.peer_manager_header().unwrap().packet_type == PacketType::RpcReq as u8 { + server_tx.send(o).await.unwrap(); + continue; + } else if o.peer_manager_header().unwrap().packet_type == PacketType::RpcResp as u8 + { + client_tx.send(o).await.unwrap(); + continue; + } + } + }); + + self.tasks.lock().unwrap().replace(tasks); + } + + pub fn rpc_client(&self) -> &Client { + &self.rpc_client + } + + pub fn rpc_server(&self) -> &Server { + &self.rpc_server + } + + pub async fn stop(&self) { + let Some(mut tasks) = self.tasks.lock().unwrap().take() else { + return; + }; + tasks.abort_all(); + while let Some(_) = tasks.join_next().await {} + } + + pub fn take_error(&self) -> Option { + self.error.lock().unwrap().take() + } + + pub async fn wait(&self) { + let Some(mut tasks) = self.tasks.lock().unwrap().take() else { + return; + }; + while let Some(_) = tasks.join_next().await { + // when any task is done, abort all tasks + tasks.abort_all(); + } + } + + pub fn is_running(&self) -> bool { + self.running.load(std::sync::atomic::Ordering::Relaxed) + } +} diff --git a/rust/easytier/src/proto/rpc_impl/client.rs b/rust/easytier/src/proto/rpc_impl/client.rs new file mode 100644 index 0000000..d40de18 --- /dev/null +++ b/rust/easytier/src/proto/rpc_impl/client.rs @@ -0,0 +1,312 @@ +use std::marker::PhantomData; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; + +use bytes::Bytes; +use dashmap::DashMap; +use prost::Message; +use tokio::sync::mpsc; +use tokio::task::JoinSet; +use tokio::time::timeout; +use tokio_stream::StreamExt; + +use crate::common::PeerId; +use crate::defer; +use crate::proto::common::{ + CompressionAlgoPb, RpcCompressionInfo, RpcDescriptor, RpcPacket, RpcRequest, RpcResponse, +}; +use crate::proto::rpc_impl::packet::{build_rpc_packet, compress_packet, decompress_packet}; +use crate::proto::rpc_types::controller::Controller; +use crate::proto::rpc_types::descriptor::MethodDescriptor; +use crate::proto::rpc_types::{ + __rt::RpcClientFactory, descriptor::ServiceDescriptor, handler::Handler, +}; + +use crate::proto::rpc_types::error::Result; +use crate::tunnel::mpsc::{MpscTunnel, MpscTunnelSender}; +use crate::tunnel::packet_def::ZCPacket; +use crate::tunnel::ring::create_ring_tunnel_pair; +use crate::tunnel::{Tunnel, TunnelError, ZCPacketStream}; + +use super::packet::PacketMerger; +use super::{RpcTransactId, Transport}; + +static CUR_TID: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(|| atomic_shim::AtomicI64::new(rand::random())); + +type RpcPacketSender = mpsc::UnboundedSender; +type RpcPacketReceiver = mpsc::UnboundedReceiver; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct InflightRequestKey { + from_peer_id: PeerId, + to_peer_id: PeerId, + transaction_id: RpcTransactId, +} + +struct InflightRequest { + sender: RpcPacketSender, + merger: PacketMerger, + start_time: std::time::Instant, +} + +#[derive(Debug, Clone, Default)] +pub struct PeerInfo { + pub peer_id: PeerId, + pub compression_info: RpcCompressionInfo, + pub last_active: Option, +} + +type InflightRequestTable = Arc>; +pub type PeerInfoTable = Arc>; + +pub struct Client { + mpsc: Mutex>>, + transport: Mutex, + inflight_requests: InflightRequestTable, + peer_info: PeerInfoTable, + tasks: Arc>>, +} + +impl Client { + pub fn new() -> Self { + let (ring_a, ring_b) = create_ring_tunnel_pair(); + Self { + mpsc: Mutex::new(MpscTunnel::new(ring_a, None)), + transport: Mutex::new(MpscTunnel::new(ring_b, None)), + inflight_requests: Arc::new(DashMap::new()), + peer_info: Arc::new(DashMap::new()), + tasks: Arc::new(Mutex::new(JoinSet::new())), + } + } + + pub fn get_transport_sink(&self) -> MpscTunnelSender { + self.transport.lock().unwrap().get_sink() + } + + pub fn get_transport_stream(&self) -> Pin> { + self.transport.lock().unwrap().get_stream() + } + + pub fn run(&self) { + let mut tasks = self.tasks.lock().unwrap(); + + let peer_infos = self.peer_info.clone(); + tasks.spawn(async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs(30)).await; + let now = std::time::Instant::now(); + peer_infos.retain(|_, v| { + if let Some(last_active) = v.last_active { + return now.duration_since(last_active) + < std::time::Duration::from_secs(120); + } + true + }); + } + }); + + let mut rx = self.mpsc.lock().unwrap().get_stream(); + let inflight_requests = self.inflight_requests.clone(); + tasks.spawn(async move { + while let Some(packet) = rx.next().await { + if let Err(err) = packet { + tracing::error!(?err, "Failed to receive packet"); + continue; + } + let packet = match RpcPacket::decode(packet.unwrap().payload()) { + Err(err) => { + tracing::error!(?err, "Failed to decode packet"); + continue; + } + Ok(packet) => packet, + }; + + if packet.is_request { + tracing::warn!(?packet, "Received non-response packet"); + continue; + } + + let key = InflightRequestKey { + from_peer_id: packet.to_peer, + to_peer_id: packet.from_peer, + transaction_id: packet.transaction_id, + }; + + let Some(mut inflight_request) = inflight_requests.get_mut(&key) else { + tracing::warn!(?key, "No inflight request found for key"); + continue; + }; + + tracing::trace!(?packet, "Received response packet"); + + let ret = inflight_request.merger.feed(packet); + match ret { + Ok(Some(rpc_packet)) => { + inflight_request.sender.send(rpc_packet).unwrap(); + } + Ok(None) => {} + Err(err) => { + tracing::error!(?err, "Failed to feed packet to merger"); + } + } + } + }); + } + + pub fn scoped_client( + &self, + from_peer_id: PeerId, + to_peer_id: PeerId, + domain_name: String, + ) -> F::ClientImpl { + #[derive(Clone)] + struct HandlerImpl { + domain_name: String, + from_peer_id: PeerId, + to_peer_id: PeerId, + zc_packet_sender: MpscTunnelSender, + inflight_requests: InflightRequestTable, + peer_info: PeerInfoTable, + _phan: PhantomData, + } + + impl HandlerImpl { + async fn do_rpc( + &self, + packets: Vec, + rx: &mut RpcPacketReceiver, + ) -> Result { + for packet in packets { + self.zc_packet_sender.send(packet).await?; + } + + Ok(rx.recv().await.ok_or(TunnelError::Shutdown)?) + } + } + + #[async_trait::async_trait] + impl Handler for HandlerImpl { + type Descriptor = F::Descriptor; + type Controller = F::Controller; + + async fn call( + &self, + mut ctrl: Self::Controller, + method: ::Method, + input: bytes::Bytes, + ) -> Result { + let transaction_id = CUR_TID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let (tx, mut rx) = mpsc::unbounded_channel(); + let key = InflightRequestKey { + from_peer_id: self.from_peer_id, + to_peer_id: self.to_peer_id, + transaction_id, + }; + + defer!(self.inflight_requests.remove(&key);); + self.inflight_requests.insert( + key.clone(), + InflightRequest { + sender: tx, + merger: PacketMerger::new(), + start_time: std::time::Instant::now(), + }, + ); + + let desc = self.service_descriptor(); + + let rpc_desc = RpcDescriptor { + domain_name: self.domain_name.clone(), + proto_name: desc.proto_name().to_string(), + service_name: desc.name().to_string(), + method_index: method.index() as u32, + }; + + let rpc_req = RpcRequest { + request: if let Some(raw_input) = ctrl.get_raw_input() { + raw_input.into() + } else { + input.into() + }, + timeout_ms: ctrl.timeout_ms(), + ..Default::default() + }; + + let peer_info = self + .peer_info + .get(&self.to_peer_id) + .map(|v| v.clone()) + .unwrap_or_default(); + let (buf, c_algo) = compress_packet( + peer_info.compression_info.accepted_algo(), + &rpc_req.encode_to_vec(), + ) + .await + .unwrap(); + + let packets = build_rpc_packet( + self.from_peer_id, + self.to_peer_id, + rpc_desc, + transaction_id, + true, + &buf, + ctrl.trace_id(), + RpcCompressionInfo { + algo: c_algo.into(), + accepted_algo: CompressionAlgoPb::Zstd.into(), + }, + ); + + let timeout_dur = std::time::Duration::from_millis(ctrl.timeout_ms() as u64); + let mut rpc_packet = timeout(timeout_dur, self.do_rpc(packets, &mut rx)).await??; + + if let Some(compression_info) = rpc_packet.compression_info { + self.peer_info.insert( + self.to_peer_id, + PeerInfo { + peer_id: self.to_peer_id, + compression_info: compression_info.clone(), + last_active: Some(std::time::Instant::now()), + }, + ); + + rpc_packet.body = + decompress_packet(compression_info.algo(), &rpc_packet.body).await?; + } + + assert_eq!(rpc_packet.transaction_id, transaction_id); + + let rpc_resp = RpcResponse::decode(Bytes::from(rpc_packet.body))?; + + if let Some(err) = &rpc_resp.error { + return Err(err.into()); + } + + let raw_output = Bytes::from(rpc_resp.response.clone()); + ctrl.set_raw_output(raw_output.clone()); + + Ok(raw_output) + } + } + + F::new(HandlerImpl:: { + domain_name: domain_name.to_string(), + from_peer_id, + to_peer_id, + zc_packet_sender: self.mpsc.lock().unwrap().get_sink(), + inflight_requests: self.inflight_requests.clone(), + peer_info: self.peer_info.clone(), + _phan: PhantomData, + }) + } + + pub fn inflight_count(&self) -> usize { + self.inflight_requests.len() + } + + pub fn peer_info_table(&self) -> PeerInfoTable { + self.peer_info.clone() + } +} diff --git a/rust/easytier/src/proto/rpc_impl/mod.rs b/rust/easytier/src/proto/rpc_impl/mod.rs new file mode 100644 index 0000000..8d7da71 --- /dev/null +++ b/rust/easytier/src/proto/rpc_impl/mod.rs @@ -0,0 +1,13 @@ +use crate::tunnel::{mpsc::MpscTunnel, Tunnel}; + +pub type RpcController = super::rpc_types::controller::BaseController; + +pub mod bidirect; +pub mod client; +pub mod packet; +pub mod server; +pub mod service_registry; +pub mod standalone; + +pub type Transport = MpscTunnel>; +pub type RpcTransactId = i64; diff --git a/rust/easytier/src/proto/rpc_impl/packet.rs b/rust/easytier/src/proto/rpc_impl/packet.rs new file mode 100644 index 0000000..9f87111 --- /dev/null +++ b/rust/easytier/src/proto/rpc_impl/packet.rs @@ -0,0 +1,202 @@ +use prost::Message as _; + +use crate::{ + common::{compressor::DefaultCompressor, PeerId}, + proto::{ + common::{CompressionAlgoPb, RpcCompressionInfo, RpcDescriptor, RpcPacket}, + rpc_types::error::Error, + }, + tunnel::packet_def::{CompressorAlgo, PacketType, ZCPacket}, +}; + +use super::RpcTransactId; + +const RPC_PACKET_CONTENT_MTU: usize = 1300; + +pub async fn compress_packet( + accepted_compression_algo: CompressionAlgoPb, + content: &[u8], +) -> Result<(Vec, CompressionAlgoPb), Error> { + let compressor = DefaultCompressor::new(); + let algo = accepted_compression_algo + .try_into() + .unwrap_or(CompressorAlgo::None); + let compressed = compressor.compress_raw(&content, algo).await?; + if compressed.len() >= content.len() { + Ok((content.to_vec(), CompressionAlgoPb::None)) + } else { + Ok((compressed, algo.try_into().unwrap())) + } +} + +pub async fn decompress_packet( + compression_algo: CompressionAlgoPb, + content: &[u8], +) -> Result, Error> { + let compressor = DefaultCompressor::new(); + let algo = compression_algo.try_into()?; + let decompressed = compressor.decompress_raw(&content, algo).await?; + Ok(decompressed) +} + +pub struct PacketMerger { + first_piece: Option, + pieces: Vec, + last_updated: std::time::Instant, +} + +impl PacketMerger { + pub fn new() -> Self { + Self { + first_piece: None, + pieces: Vec::new(), + last_updated: std::time::Instant::now(), + } + } + + fn try_merge_pieces(&self) -> Option { + if self.first_piece.is_none() || self.pieces.is_empty() { + return None; + } + + for p in &self.pieces { + // some piece is missing + if p.total_pieces == 0 { + return None; + } + } + + // all pieces are received + let mut body = Vec::new(); + for p in &self.pieces { + body.extend_from_slice(&p.body); + } + + // only the first packet contains the complete info + let mut tmpl_packet = self.pieces[0].clone(); + tmpl_packet.total_pieces = 1; + tmpl_packet.piece_idx = 0; + tmpl_packet.body = body; + + Some(tmpl_packet) + } + + pub fn feed(&mut self, rpc_packet: RpcPacket) -> Result, Error> { + let total_pieces = rpc_packet.total_pieces; + let piece_idx = rpc_packet.piece_idx; + + // for compatibility with old version + if total_pieces == 0 && piece_idx == 0 { + return Ok(Some(rpc_packet)); + } + + if rpc_packet.piece_idx == 0 && rpc_packet.descriptor.is_none() { + return Err(Error::MalformatRpcPacket( + "descriptor is missing".to_owned(), + )); + } + + // about 32MB max size + if total_pieces > 32 * 1024 || total_pieces == 0 { + return Err(Error::MalformatRpcPacket(format!( + "total_pieces is invalid: {}", + total_pieces + ))); + } + + if piece_idx >= total_pieces { + return Err(Error::MalformatRpcPacket( + "piece_idx >= total_pieces".to_owned(), + )); + } + + if self.first_piece.is_none() + || self.first_piece.as_ref().unwrap().transaction_id != rpc_packet.transaction_id + || self.first_piece.as_ref().unwrap().from_peer != rpc_packet.from_peer + { + self.first_piece = Some(rpc_packet.clone()); + self.pieces.clear(); + tracing::trace!(?rpc_packet, "got first piece"); + } + + self.pieces + .resize(total_pieces as usize, Default::default()); + self.pieces[piece_idx as usize] = rpc_packet; + + self.last_updated = std::time::Instant::now(); + + Ok(self.try_merge_pieces()) + } + + pub fn last_updated(&self) -> std::time::Instant { + self.last_updated + } +} + +pub fn build_rpc_packet( + from_peer: PeerId, + to_peer: PeerId, + rpc_desc: RpcDescriptor, + transaction_id: RpcTransactId, + is_req: bool, + content: &Vec, + trace_id: i32, + compression_info: RpcCompressionInfo, +) -> Vec { + let mut ret = Vec::new(); + let content_mtu = RPC_PACKET_CONTENT_MTU; + let total_pieces = (content.len() + content_mtu - 1) / content_mtu; + let mut cur_offset = 0; + while cur_offset < content.len() || content.len() == 0 { + let mut cur_len = content_mtu; + if cur_offset + cur_len > content.len() { + cur_len = content.len() - cur_offset; + } + + let mut cur_content = Vec::new(); + cur_content.extend_from_slice(&content[cur_offset..cur_offset + cur_len]); + + let cur_packet = RpcPacket { + from_peer, + to_peer, + descriptor: if cur_offset == 0 + || compression_info.algo == CompressionAlgoPb::None as i32 + { + // old version must have descriptor on every piece + Some(rpc_desc.clone()) + } else { + None + }, + is_request: is_req, + total_pieces: total_pieces as u32, + piece_idx: (cur_offset / content_mtu) as u32, + transaction_id, + body: cur_content, + trace_id, + compression_info: if cur_offset == 0 { + Some(compression_info.clone()) + } else { + None + }, + }; + cur_offset += cur_len; + + let packet_type = if is_req { + PacketType::RpcReq + } else { + PacketType::RpcResp + }; + + let mut buf = Vec::new(); + cur_packet.encode(&mut buf).unwrap(); + let mut zc_packet = ZCPacket::new_with_payload(&buf); + zc_packet.fill_peer_manager_hdr(from_peer, to_peer, packet_type as u8); + ret.push(zc_packet); + + if content.len() == 0 { + break; + } + } + + ret +} diff --git a/rust/easytier/src/proto/rpc_impl/server.rs b/rust/easytier/src/proto/rpc_impl/server.rs new file mode 100644 index 0000000..63a1b5e --- /dev/null +++ b/rust/easytier/src/proto/rpc_impl/server.rs @@ -0,0 +1,231 @@ +use std::{ + pin::Pin, + sync::{Arc, Mutex}, +}; + +use bytes::Bytes; +use dashmap::DashMap; +use prost::Message; +use tokio::{task::JoinSet, time::timeout}; +use tokio_stream::StreamExt; + +use crate::{ + common::{join_joinset_background, PeerId}, + proto::{ + common::{self, CompressionAlgoPb, RpcCompressionInfo, RpcPacket, RpcRequest, RpcResponse}, + rpc_types::{controller::Controller, error::Result}, + }, + tunnel::{ + mpsc::{MpscTunnel, MpscTunnelSender}, + ring::create_ring_tunnel_pair, + Tunnel, ZCPacketStream, + }, +}; + +use super::{ + packet::{build_rpc_packet, compress_packet, decompress_packet, PacketMerger}, + service_registry::ServiceRegistry, + RpcController, Transport, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct PacketMergerKey { + from_peer_id: PeerId, + transaction_id: i64, +} + +pub struct Server { + registry: Arc, + + mpsc: Mutex>>>, + + transport: Mutex, + + tasks: Arc>>, + packet_mergers: Arc>, +} + +impl Server { + pub fn new() -> Self { + Server::new_with_registry(Arc::new(ServiceRegistry::new())) + } + + pub fn new_with_registry(registry: Arc) -> Self { + let (ring_a, ring_b) = create_ring_tunnel_pair(); + + Self { + registry, + mpsc: Mutex::new(Some(MpscTunnel::new(ring_a, None))), + transport: Mutex::new(MpscTunnel::new(ring_b, None)), + tasks: Arc::new(Mutex::new(JoinSet::new())), + packet_mergers: Arc::new(DashMap::new()), + } + } + + pub fn registry(&self) -> &ServiceRegistry { + &self.registry + } + + pub fn get_transport_sink(&self) -> MpscTunnelSender { + self.transport.lock().unwrap().get_sink() + } + + pub fn get_transport_stream(&self) -> Pin> { + self.transport.lock().unwrap().get_stream() + } + + pub fn run(&self) { + let tasks = self.tasks.clone(); + join_joinset_background(tasks.clone(), "rpc server".to_string()); + + let mpsc = self.mpsc.lock().unwrap().take().unwrap(); + + let packet_merges = self.packet_mergers.clone(); + let reg = self.registry.clone(); + let t = tasks.clone(); + tasks.lock().unwrap().spawn(async move { + let mut mpsc = mpsc; + let mut rx = mpsc.get_stream(); + + while let Some(packet) = rx.next().await { + if let Err(err) = packet { + tracing::error!(?err, "Failed to receive packet"); + continue; + } + let packet = match common::RpcPacket::decode(packet.unwrap().payload()) { + Err(err) => { + tracing::error!(?err, "Failed to decode packet"); + continue; + } + Ok(packet) => packet, + }; + + if !packet.is_request { + tracing::warn!(?packet, "Received non-request packet"); + continue; + } + + let key = PacketMergerKey { + from_peer_id: packet.from_peer, + transaction_id: packet.transaction_id, + }; + + tracing::trace!(?key, ?packet, "Received request packet"); + + let ret = packet_merges + .entry(key.clone()) + .or_insert_with(PacketMerger::new) + .feed(packet); + + match ret { + Ok(Some(packet)) => { + packet_merges.remove(&key); + t.lock().unwrap().spawn(Self::handle_rpc( + mpsc.get_sink(), + packet, + reg.clone(), + )); + } + Ok(None) => {} + Err(err) => { + tracing::error!("Failed to feed packet to merger, {}", err.to_string()); + } + } + } + }); + + let packet_mergers = self.packet_mergers.clone(); + tasks.lock().unwrap().spawn(async move { + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + packet_mergers.retain(|_, v| v.last_updated().elapsed().as_secs() < 10); + } + }); + } + + async fn handle_rpc_request(packet: RpcPacket, reg: Arc) -> Result { + let body = if let Some(compression_info) = packet.compression_info { + decompress_packet( + compression_info.algo.try_into().unwrap_or_default(), + &packet.body, + ) + .await? + } else { + packet.body + }; + let rpc_request = RpcRequest::decode(Bytes::from(body))?; + let timeout_duration = std::time::Duration::from_millis(rpc_request.timeout_ms as u64); + let mut ctrl = RpcController::default(); + let raw_req = Bytes::from(rpc_request.request); + ctrl.set_raw_input(raw_req.clone()); + let ret = timeout( + timeout_duration, + reg.call_method(packet.descriptor.unwrap(), ctrl.clone(), raw_req), + ) + .await??; + if let Some(raw_output) = ctrl.get_raw_output() { + Ok(raw_output) + } else { + Ok(ret) + } + } + + async fn handle_rpc(sender: MpscTunnelSender, packet: RpcPacket, reg: Arc) { + let from_peer = packet.from_peer; + let to_peer = packet.to_peer; + let transaction_id = packet.transaction_id; + let trace_id = packet.trace_id; + let desc = packet.descriptor.clone().unwrap(); + + let mut resp_msg = RpcResponse::default(); + let now = std::time::Instant::now(); + + let compression_info = packet.compression_info.clone(); + let resp_bytes = Self::handle_rpc_request(packet, reg).await; + + match &resp_bytes { + Ok(r) => { + resp_msg.response = r.clone().into(); + } + Err(err) => { + resp_msg.error = Some(err.into()); + } + }; + resp_msg.runtime_us = now.elapsed().as_micros() as u64; + + let (compressed_resp, algo) = compress_packet( + compression_info.unwrap_or_default().accepted_algo(), + &resp_msg.encode_to_vec(), + ) + .await + .unwrap(); + + let packets = build_rpc_packet( + to_peer, + from_peer, + desc, + transaction_id, + false, + &compressed_resp, + trace_id, + RpcCompressionInfo { + algo: algo.into(), + accepted_algo: CompressionAlgoPb::Zstd.into(), + }, + ); + + for packet in packets { + if let Err(err) = sender.send(packet).await { + tracing::error!(?err, "Failed to send response packet"); + } + } + } + + pub fn inflight_count(&self) -> usize { + self.packet_mergers.len() + } + + pub fn close(&self) { + self.transport.lock().unwrap().close(); + } +} diff --git a/rust/easytier/src/proto/rpc_impl/service_registry.rs b/rust/easytier/src/proto/rpc_impl/service_registry.rs new file mode 100644 index 0000000..1ca440d --- /dev/null +++ b/rust/easytier/src/proto/rpc_impl/service_registry.rs @@ -0,0 +1,117 @@ +use std::sync::Arc; + +use dashmap::DashMap; + +use crate::proto::common::RpcDescriptor; +use crate::proto::rpc_types; +use crate::proto::rpc_types::descriptor::ServiceDescriptor; +use crate::proto::rpc_types::handler::{Handler, HandlerExt}; + +use super::RpcController; + +#[derive(Clone, PartialEq, Eq, Debug, Hash)] +pub struct ServiceKey { + pub domain_name: String, + pub service_name: String, + pub proto_name: String, +} + +impl From<&RpcDescriptor> for ServiceKey { + fn from(desc: &RpcDescriptor) -> Self { + Self { + domain_name: desc.domain_name.to_string(), + service_name: desc.service_name.to_string(), + proto_name: desc.proto_name.to_string(), + } + } +} + +#[derive(Clone)] +struct ServiceEntry { + service: Arc>>, +} + +impl ServiceEntry { + fn new>(h: H) -> Self { + Self { + service: Arc::new(Box::new(h)), + } + } + + async fn call_method( + &self, + ctrl: RpcController, + method_index: u8, + input: bytes::Bytes, + ) -> rpc_types::error::Result { + self.service.call_method(ctrl, method_index, input).await + } +} + +pub struct ServiceRegistry { + table: DashMap, +} + +impl ServiceRegistry { + pub fn new() -> Self { + Self { + table: DashMap::new(), + } + } + + pub fn replace_registry(&self, registry: &ServiceRegistry) { + self.table.clear(); + for item in registry.table.iter() { + let (k, v) = item.pair(); + self.table.insert(k.clone(), v.clone()); + } + } + + pub fn register>(&self, h: H, domain_name: &str) { + let desc = h.service_descriptor(); + let key = ServiceKey { + domain_name: domain_name.to_string(), + service_name: desc.name().to_string(), + proto_name: desc.proto_name().to_string(), + }; + let entry = ServiceEntry::new(h); + self.table.insert(key, entry); + } + + pub fn unregister>( + &self, + h: H, + domain_name: &str, + ) -> Option<()> { + let desc = h.service_descriptor(); + let key = ServiceKey { + domain_name: domain_name.to_string(), + service_name: desc.name().to_string(), + proto_name: desc.proto_name().to_string(), + }; + self.table.remove(&key).map(|_| ()) + } + + pub fn unregister_by_domain(&self, domain_name: &str) { + self.table.retain(|k, _| k.domain_name != domain_name); + } + + pub async fn call_method( + &self, + rpc_desc: RpcDescriptor, + ctrl: RpcController, + input: bytes::Bytes, + ) -> rpc_types::error::Result { + let service_key = ServiceKey::from(&rpc_desc); + let method_index = rpc_desc.method_index as u8; + let entry = self + .table + .get(&service_key) + .ok_or(rpc_types::error::Error::InvalidServiceKey( + service_key.service_name.clone(), + service_key.proto_name.clone(), + ))? + .clone(); + entry.call_method(ctrl, method_index, input).await + } +} diff --git a/rust/easytier/src/proto/rpc_impl/standalone.rs b/rust/easytier/src/proto/rpc_impl/standalone.rs new file mode 100644 index 0000000..c86c5b0 --- /dev/null +++ b/rust/easytier/src/proto/rpc_impl/standalone.rs @@ -0,0 +1,149 @@ +use std::{ + sync::{atomic::AtomicU32, Arc, Mutex}, + time::Duration, +}; + +use anyhow::Context as _; +use tokio::task::JoinSet; + +use crate::{ + common::join_joinset_background, + proto::{ + rpc_impl::bidirect::BidirectRpcManager, + rpc_types::{__rt::RpcClientFactory, error::Error}, + }, + tunnel::{Tunnel, TunnelConnector, TunnelListener}, +}; + +use super::service_registry::ServiceRegistry; + +pub struct StandAloneServer { + registry: Arc, + listener: Option, + inflight_server: Arc, + tasks: Arc>>, +} + +impl StandAloneServer { + pub fn new(listener: L) -> Self { + StandAloneServer { + registry: Arc::new(ServiceRegistry::new()), + listener: Some(listener), + inflight_server: Arc::new(AtomicU32::new(0)), + tasks: Arc::new(Mutex::new(JoinSet::new())), + } + } + + pub fn registry(&self) -> &ServiceRegistry { + &self.registry + } + + async fn serve_loop( + listener: &mut L, + inflight: Arc, + registry: Arc, + tasks: Arc>>, + ) -> Result<(), Error> { + listener + .listen() + .await + .with_context(|| "failed to listen")?; + + loop { + let tunnel = listener.accept().await?; + let registry = registry.clone(); + let inflight_server = inflight.clone(); + inflight_server.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + tasks.lock().unwrap().spawn(async move { + let server = + BidirectRpcManager::new().set_rx_timeout(Some(Duration::from_secs(60))); + server.rpc_server().registry().replace_registry(®istry); + server.run_with_tunnel(tunnel); + server.wait().await; + inflight_server.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + }); + } + } + + pub async fn serve(&mut self) -> Result<(), Error> { + let tasks = self.tasks.clone(); + let mut listener = self.listener.take().unwrap(); + let registry = self.registry.clone(); + + join_joinset_background(tasks.clone(), "standalone server tasks".to_string()); + + let inflight_server = self.inflight_server.clone(); + + self.tasks.lock().unwrap().spawn(async move { + loop { + let ret = Self::serve_loop( + &mut listener, + inflight_server.clone(), + registry.clone(), + tasks.clone(), + ) + .await; + if let Err(e) = ret { + tracing::error!(?e, url = ?listener.local_url(), "serve_loop exit unexpectedly"); + println!("standalone serve_loop exit unexpectedly: {:?}", e); + } + + tokio::time::sleep(Duration::from_secs(1)).await; + } + }); + + Ok(()) + } + + pub fn inflight_server(&self) -> u32 { + self.inflight_server + .load(std::sync::atomic::Ordering::Relaxed) + } +} + +pub struct StandAloneClient { + connector: C, + client: Option, +} + +impl StandAloneClient { + pub fn new(connector: C) -> Self { + StandAloneClient { + connector, + client: None, + } + } + + async fn connect(&mut self) -> Result, Error> { + Ok(self.connector.connect().await.with_context(|| { + format!( + "failed to connect to server: {:?}", + self.connector.remote_url() + ) + })?) + } + + pub async fn scoped_client( + &mut self, + domain_name: String, + ) -> Result { + let mut c = self.client.take(); + let error = c.as_ref().and_then(|c| c.take_error()); + if c.is_none() || error.is_some() { + tracing::info!("reconnect due to error: {:?}", error); + let tunnel = self.connect().await?; + let mgr = BidirectRpcManager::new().set_rx_timeout(Some(Duration::from_secs(60))); + mgr.run_with_tunnel(tunnel); + c = Some(mgr); + } + + self.client = c; + + Ok(self + .client + .as_ref() + .unwrap() + .rpc_client() + .scoped_client::(1, 1, domain_name)) + } +} diff --git a/rust/easytier/src/proto/rpc_types/__rt.rs b/rust/easytier/src/proto/rpc_types/__rt.rs new file mode 100644 index 0000000..6c4e00b --- /dev/null +++ b/rust/easytier/src/proto/rpc_types/__rt.rs @@ -0,0 +1,57 @@ +//! Utility functions used by generated code; this is *not* part of the crate's public API! +use bytes; +use prost; + +use super::controller; +use super::descriptor; +use super::descriptor::ServiceDescriptor; +use super::error; +use super::handler; +use super::handler::Handler; + +/// Efficiently decode a particular message type from a byte buffer. +pub fn decode(buf: bytes::Bytes) -> error::Result +where + M: prost::Message + Default, +{ + let message = prost::Message::decode(buf)?; + Ok(message) +} + +/// Efficiently encode a particular message into a byte buffer. +pub fn encode(message: M) -> error::Result +where + M: prost::Message, +{ + let len = prost::Message::encoded_len(&message); + let mut buf = ::bytes::BytesMut::with_capacity(len); + prost::Message::encode(&message, &mut buf)?; + Ok(buf.freeze()) +} + +pub async fn call_method( + handler: H, + ctrl: H::Controller, + method: ::Method, + input: I, +) -> super::error::Result +where + H: handler::Handler, + I: prost::Message, + O: prost::Message + Default, +{ + type Error = super::error::Error; + let input_bytes = encode(input)?; + let ret_msg = handler.call(ctrl, method, input_bytes).await?; + decode(ret_msg) +} + +pub trait RpcClientFactory: Clone + Send + Sync + 'static { + type Descriptor: ServiceDescriptor + Default; + type ClientImpl; + type Controller: controller::Controller; + + fn new( + handler: impl Handler, + ) -> Self::ClientImpl; +} diff --git a/rust/easytier/src/proto/rpc_types/controller.rs b/rust/easytier/src/proto/rpc_types/controller.rs new file mode 100644 index 0000000..0259ad4 --- /dev/null +++ b/rust/easytier/src/proto/rpc_types/controller.rs @@ -0,0 +1,88 @@ +use std::sync::{Arc, Mutex}; + +use bytes::Bytes; + +// Controller must impl clone and all cloned controllers share the same data +pub trait Controller: Send + Sync + Clone + 'static { + fn timeout_ms(&self) -> i32 { + 5000 + } + + fn set_timeout_ms(&mut self, _timeout_ms: i32) {} + + fn set_trace_id(&mut self, _trace_id: i32) {} + + fn trace_id(&self) -> i32 { + 0 + } + + fn set_raw_input(&mut self, _raw_input: Bytes) {} + fn get_raw_input(&self) -> Option { + None + } + + fn set_raw_output(&mut self, _raw_output: Bytes) {} + fn get_raw_output(&self) -> Option { + None + } +} + +#[derive(Debug)] +pub struct BaseControllerRawData { + pub raw_input: Option, + pub raw_output: Option, +} + +#[derive(Debug, Clone)] +pub struct BaseController { + pub timeout_ms: i32, + pub trace_id: i32, + pub raw_data: Arc>, +} + +impl Controller for BaseController { + fn timeout_ms(&self) -> i32 { + self.timeout_ms + } + + fn set_timeout_ms(&mut self, timeout_ms: i32) { + self.timeout_ms = timeout_ms; + } + + fn set_trace_id(&mut self, trace_id: i32) { + self.trace_id = trace_id; + } + + fn trace_id(&self) -> i32 { + self.trace_id + } + + fn set_raw_input(&mut self, raw_input: Bytes) { + self.raw_data.lock().unwrap().raw_input = Some(raw_input); + } + + fn get_raw_input(&self) -> Option { + self.raw_data.lock().unwrap().raw_input.clone() + } + + fn set_raw_output(&mut self, raw_output: Bytes) { + self.raw_data.lock().unwrap().raw_output = Some(raw_output); + } + + fn get_raw_output(&self) -> Option { + self.raw_data.lock().unwrap().raw_output.clone() + } +} + +impl Default for BaseController { + fn default() -> Self { + Self { + timeout_ms: 5000, + trace_id: 0, + raw_data: Arc::new(Mutex::new(BaseControllerRawData { + raw_input: None, + raw_output: None, + })), + } + } +} diff --git a/rust/easytier/src/proto/rpc_types/descriptor.rs b/rust/easytier/src/proto/rpc_types/descriptor.rs new file mode 100644 index 0000000..c051db8 --- /dev/null +++ b/rust/easytier/src/proto/rpc_types/descriptor.rs @@ -0,0 +1,50 @@ +//! Traits for defining generic service descriptor definitions. +//! +//! These traits are built on the assumption that some form of code generation is being used (e.g. +//! using only `&'static str`s) but it's of course possible to implement these traits manually. +use std::any; +use std::fmt; + +/// A descriptor for an available RPC service. +pub trait ServiceDescriptor: Clone + fmt::Debug + Send + Sync { + /// The associated type of method descriptors. + type Method: MethodDescriptor + fmt::Debug + TryFrom; + + /// The name of the service, used in Rust code and perhaps for human readability. + fn name(&self) -> &'static str; + + /// The raw protobuf name of the service. + fn proto_name(&self) -> &'static str; + + /// The package name of the service. + fn package(&self) -> &'static str { + "" + } + + /// All of the available methods on the service. + fn methods(&self) -> &'static [Self::Method]; +} + +/// A descriptor for a method available on an RPC service. +pub trait MethodDescriptor: Clone + Copy + fmt::Debug + Send + Sync { + /// The name of the service, used in Rust code and perhaps for human readability. + fn name(&self) -> &'static str; + + /// The raw protobuf name of the service. + fn proto_name(&self) -> &'static str; + + /// The Rust `TypeId` for the input that this method accepts. + fn input_type(&self) -> any::TypeId; + + /// The raw protobuf name for the input type that this method accepts. + fn input_proto_type(&self) -> &'static str; + + /// The Rust `TypeId` for the output that this method produces. + fn output_type(&self) -> any::TypeId; + + /// The raw protobuf name for the output type that this method produces. + fn output_proto_type(&self) -> &'static str; + + /// The index of the method in the service descriptor. + fn index(&self) -> u8; +} diff --git a/rust/easytier/src/proto/rpc_types/error.rs b/rust/easytier/src/proto/rpc_types/error.rs new file mode 100644 index 0000000..ba86d75 --- /dev/null +++ b/rust/easytier/src/proto/rpc_types/error.rs @@ -0,0 +1,37 @@ +//! Error type definitions for errors that can occur during RPC interactions. +use std::result; + +use prost; +use thiserror; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("rust tun error {0}")] + ExecutionError(#[from] anyhow::Error), + + #[error("Decode error: {0}")] + DecodeError(#[from] prost::DecodeError), + + #[error("Encode error: {0}")] + EncodeError(#[from] prost::EncodeError), + + #[error("Invalid method index: {0}, service: {1}")] + InvalidMethodIndex(u8, String), + + #[error("Invalid service name: {0}, proto name: {1}")] + InvalidServiceKey(String, String), + + #[error("Invalid packet: {0}")] + MalformatRpcPacket(String), + + #[error("Timeout: {0}")] + Timeout(#[from] tokio::time::error::Elapsed), + + #[error("Tunnel error: {0}")] + TunnelError(#[from] crate::tunnel::TunnelError), + + #[error("Shutdown")] + Shutdown, +} + +pub type Result = result::Result; diff --git a/rust/easytier/src/proto/rpc_types/handler.rs b/rust/easytier/src/proto/rpc_types/handler.rs new file mode 100644 index 0000000..b893cd5 --- /dev/null +++ b/rust/easytier/src/proto/rpc_types/handler.rs @@ -0,0 +1,67 @@ +//! Traits for defining generic RPC handlers. +use super::{ + controller::Controller, + descriptor::{self, ServiceDescriptor}, +}; +use bytes; + +/// An implementation of a specific RPC handler. +/// +/// This can be an actual implementation of a service, or something that will send a request over +/// a network to fulfill a request. +#[async_trait::async_trait] +pub trait Handler: Clone + Send + Sync + 'static { + /// The service descriptor for the service whose requests this handler can handle. + type Descriptor: descriptor::ServiceDescriptor + Default; + + type Controller: super::controller::Controller; + /// + + /// Perform a raw call to the specified service and method. + async fn call( + &self, + ctrl: Self::Controller, + method: ::Method, + input: bytes::Bytes, + ) -> super::error::Result; + + fn service_descriptor(&self) -> Self::Descriptor { + Self::Descriptor::default() + } + + fn get_method_from_index( + &self, + index: u8, + ) -> super::error::Result<::Method> { + let desc = self.service_descriptor(); + ::Method::try_from(index) + .map_err(|_| super::error::Error::InvalidMethodIndex(index, desc.name().to_string())) + } +} + +#[async_trait::async_trait] +pub trait HandlerExt: Send + Sync + 'static { + type Controller; + + async fn call_method( + &self, + ctrl: Self::Controller, + method_index: u8, + input: bytes::Bytes, + ) -> super::error::Result; +} + +#[async_trait::async_trait] +impl> HandlerExt for T { + type Controller = C; + + async fn call_method( + &self, + ctrl: Self::Controller, + method_index: u8, + input: bytes::Bytes, + ) -> super::error::Result { + let method = self.get_method_from_index(method_index)?; + self.call(ctrl, method, input).await + } +} diff --git a/rust/easytier/src/proto/rpc_types/mod.rs b/rust/easytier/src/proto/rpc_types/mod.rs new file mode 100644 index 0000000..1f78d92 --- /dev/null +++ b/rust/easytier/src/proto/rpc_types/mod.rs @@ -0,0 +1,5 @@ +pub mod __rt; +pub mod controller; +pub mod descriptor; +pub mod error; +pub mod handler; diff --git a/rust/easytier/src/proto/tests.proto b/rust/easytier/src/proto/tests.proto new file mode 100644 index 0000000..a0c1fc8 --- /dev/null +++ b/rust/easytier/src/proto/tests.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package tests; + +/// The Greeting service. This service is used to generate greetings for various +/// use-cases. +service Greeting { + // Generates a "hello" greeting based on the supplied info. + rpc SayHello(SayHelloRequest) returns (SayHelloResponse); + // Generates a "goodbye" greeting based on the supplied info. + rpc SayGoodbye(SayGoodbyeRequest) returns (SayGoodbyeResponse); +} + +// The request for an `Greeting.SayHello` call. +message SayHelloRequest { string name = 1; } + +// The response for an `Greeting.SayHello` call. +message SayHelloResponse { string greeting = 1; } + +// The request for an `Greeting.SayGoodbye` call. +message SayGoodbyeRequest { string name = 1; } + +// The response for an `Greeting.SayGoodbye` call. +message SayGoodbyeResponse { string greeting = 1; } diff --git a/rust/easytier/src/proto/tests.rs b/rust/easytier/src/proto/tests.rs new file mode 100644 index 0000000..eff4bdd --- /dev/null +++ b/rust/easytier/src/proto/tests.rs @@ -0,0 +1,402 @@ +include!(concat!(env!("OUT_DIR"), "/tests.rs")); + +use std::sync::{Arc, Mutex}; + +use futures::StreamExt as _; +use tokio::task::JoinSet; + +use super::rpc_impl::RpcController; + +#[derive(Clone)] +pub struct GreetingService { + pub delay_ms: u64, + pub prefix: String, +} + +#[async_trait::async_trait] +impl Greeting for GreetingService { + type Controller = RpcController; + async fn say_hello( + &self, + _ctrl: Self::Controller, + input: SayHelloRequest, + ) -> crate::proto::rpc_types::error::Result { + let resp = SayHelloResponse { + greeting: format!("{} {}!", self.prefix, input.name), + }; + tokio::time::sleep(std::time::Duration::from_millis(self.delay_ms)).await; + Ok(resp) + } + /// Generates a "goodbye" greeting based on the supplied info. + async fn say_goodbye( + &self, + _ctrl: Self::Controller, + input: SayGoodbyeRequest, + ) -> crate::proto::rpc_types::error::Result { + let resp = SayGoodbyeResponse { + greeting: format!("Goodbye, {}!", input.name), + }; + tokio::time::sleep(std::time::Duration::from_millis(self.delay_ms)).await; + Ok(resp) + } +} + +use crate::proto::common::{CompressionAlgoPb, RpcCompressionInfo}; +use crate::proto::rpc_impl::client::Client; +use crate::proto::rpc_impl::server::Server; + +struct TestContext { + client: Client, + server: Server, + tasks: Arc>>, +} + +impl TestContext { + fn new() -> Self { + let rpc_server = Server::new(); + rpc_server.run(); + + let client = Client::new(); + client.run(); + + let tasks = Arc::new(Mutex::new(JoinSet::new())); + let (mut rx, tx) = ( + rpc_server.get_transport_stream(), + client.get_transport_sink(), + ); + + tasks.lock().unwrap().spawn(async move { + while let Some(Ok(packet)) = rx.next().await { + if let Err(err) = tx.send(packet).await { + println!("{:?}", err); + break; + } + } + }); + + let (mut rx, tx) = ( + client.get_transport_stream(), + rpc_server.get_transport_sink(), + ); + tasks.lock().unwrap().spawn(async move { + while let Some(Ok(packet)) = rx.next().await { + if let Err(err) = tx.send(packet).await { + println!("{:?}", err); + break; + } + } + }); + + Self { + client, + server: rpc_server, + tasks, + } + } +} + +fn random_string(len: usize) -> String { + use rand::distributions::Alphanumeric; + use rand::Rng; + let mut rng = rand::thread_rng(); + let s: Vec = std::iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .take(len) + .collect(); + String::from_utf8(s).unwrap() +} + +#[tokio::test] +async fn rpc_basic_test() { + // enable_log(); + let ctx = TestContext::new(); + + let server = GreetingServer::new(GreetingService { + delay_ms: 0, + prefix: "Hello".to_string(), + }); + ctx.server.registry().register(server, ""); + + let out = ctx + .client + .scoped_client::>(1, 1, "".to_string()); + + // // small size req and resp + + let ctrl = RpcController::default(); + let input = SayHelloRequest { + name: "world".to_string(), + }; + let ret = out.say_hello(ctrl, input).await; + assert_eq!(ret.unwrap().greeting, "Hello world!"); + + assert_eq!(1, ctx.client.peer_info_table().len()); + let first_peer_info = ctx.client.peer_info_table().iter().next().unwrap().clone(); + assert_eq!( + first_peer_info.compression_info.accepted_algo(), + CompressionAlgoPb::Zstd, + ); + + println!("{:?}", ctx.client.peer_info_table()); + + let ctrl = RpcController::default(); + let input = SayGoodbyeRequest { + name: "world".to_string(), + }; + let ret = out.say_goodbye(ctrl, input).await; + assert_eq!(ret.unwrap().greeting, "Goodbye, world!"); + + // large size req and resp + let ctrl = RpcController::default(); + let name = random_string(20 * 1024 * 1024); + let input = SayGoodbyeRequest { name: name.clone() }; + let ret = out.say_goodbye(ctrl, input).await; + assert_eq!(ret.unwrap().greeting, format!("Goodbye, {}!", name)); + + assert_eq!(0, ctx.client.inflight_count()); + assert_eq!(0, ctx.server.inflight_count()); + + let first_peer_info = ctx.client.peer_info_table().iter().next().unwrap().clone(); + assert_eq!( + first_peer_info.compression_info, + RpcCompressionInfo { + algo: CompressionAlgoPb::Zstd.into(), + accepted_algo: CompressionAlgoPb::Zstd.into(), + } + ); +} + +#[tokio::test] +async fn rpc_timeout_test() { + let ctx = TestContext::new(); + + let server = GreetingServer::new(GreetingService { + delay_ms: 10000, + prefix: "Hello".to_string(), + }); + ctx.server.registry().register(server, "test"); + + let out = ctx + .client + .scoped_client::>(1, 1, "test".to_string()); + + let ctrl = RpcController::default(); + let input = SayHelloRequest { + name: "world".to_string(), + }; + let ret = out.say_hello(ctrl, input).await; + assert!(ret.is_err()); + assert!(matches!( + ret.unwrap_err(), + crate::proto::rpc_types::error::Error::Timeout(_) + )); + + assert_eq!(0, ctx.client.inflight_count()); + assert_eq!(0, ctx.server.inflight_count()); +} + +#[tokio::test] +async fn rpc_tunnel_stuck_test() { + use crate::proto::rpc_types; + use crate::tunnel::ring::RING_TUNNEL_CAP; + + let rpc_server = Server::new(); + rpc_server.run(); + let server = GreetingServer::new(GreetingService { + delay_ms: 0, + prefix: "Hello".to_string(), + }); + rpc_server.registry().register(server, "test"); + + let client = Client::new(); + client.run(); + + let rpc_tasks = Arc::new(Mutex::new(JoinSet::new())); + let (mut rx, tx) = ( + rpc_server.get_transport_stream(), + client.get_transport_sink(), + ); + + rpc_tasks.lock().unwrap().spawn(async move { + while let Some(Ok(packet)) = rx.next().await { + if let Err(err) = tx.send(packet).await { + println!("{:?}", err); + break; + } + } + }); + + // mock server is stuck (no task to do forwards) + + let mut tasks = JoinSet::new(); + for _ in 0..RING_TUNNEL_CAP + 15 { + let out = + client.scoped_client::>(1, 1, "test".to_string()); + tasks.spawn(async move { + let mut ctrl = RpcController::default(); + ctrl.timeout_ms = 1000; + + let input = SayHelloRequest { + name: "world".to_string(), + }; + + out.say_hello(ctrl, input).await + }); + } + while let Some(ret) = tasks.join_next().await { + assert!(matches!(ret, Ok(Err(rpc_types::error::Error::Timeout(_))))); + } + + // start server consumer, new requests should be processed + let (mut rx, tx) = ( + client.get_transport_stream(), + rpc_server.get_transport_sink(), + ); + rpc_tasks.lock().unwrap().spawn(async move { + while let Some(Ok(packet)) = rx.next().await { + if let Err(err) = tx.send(packet).await { + println!("{:?}", err); + break; + } + } + }); + + let out = + client.scoped_client::>(1, 1, "test".to_string()); + let mut ctrl = RpcController::default(); + ctrl.timeout_ms = 1000; + let input = SayHelloRequest { + name: "fuck world".to_string(), + }; + let ret = out.say_hello(ctrl, input).await.unwrap(); + assert_eq!(ret.greeting, "Hello fuck world!"); +} + +#[tokio::test] +async fn standalone_rpc_test() { + use crate::proto::rpc_impl::standalone::{StandAloneClient, StandAloneServer}; + use crate::tunnel::tcp::{TcpTunnelConnector, TcpTunnelListener}; + + let mut server = StandAloneServer::new(TcpTunnelListener::new( + "tcp://0.0.0.0:33455".parse().unwrap(), + )); + let service = GreetingServer::new(GreetingService { + delay_ms: 0, + prefix: "Hello".to_string(), + }); + server.registry().register(service, "test"); + server.serve().await.unwrap(); + + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + + let mut client = StandAloneClient::new(TcpTunnelConnector::new( + "tcp://127.0.0.1:33455".parse().unwrap(), + )); + + let out = client + .scoped_client::>("test".to_string()) + .await + .unwrap(); + + let ctrl = RpcController::default(); + let input = SayHelloRequest { + name: "world".to_string(), + }; + let ret = out.say_hello(ctrl, input).await; + assert_eq!(ret.unwrap().greeting, "Hello world!"); + + let out = client + .scoped_client::>("test".to_string()) + .await + .unwrap(); + + let ctrl = RpcController::default(); + let input = SayGoodbyeRequest { + name: "world".to_string(), + }; + let ret = out.say_goodbye(ctrl, input).await; + assert_eq!(ret.unwrap().greeting, "Goodbye, world!"); + + drop(client); + + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + assert_eq!(0, server.inflight_server()); +} + +#[tokio::test] +async fn test_bidirect_rpc_manager() { + use crate::common::scoped_task::ScopedTask; + use crate::proto::rpc_impl::bidirect::BidirectRpcManager; + use crate::tunnel::tcp::{TcpTunnelConnector, TcpTunnelListener}; + use crate::tunnel::{TunnelConnector, TunnelListener}; + use tokio::sync::Notify; + + let c = BidirectRpcManager::new(); + let s = BidirectRpcManager::new(); + + let service = GreetingServer::new(GreetingService { + delay_ms: 0, + prefix: "Hello Client".to_string(), + }); + c.rpc_server().registry().register(service, "test"); + + let service = GreetingServer::new(GreetingService { + delay_ms: 0, + prefix: "Hello Server".to_string(), + }); + s.rpc_server().registry().register(service, "test"); + + let server_test_done = Arc::new(Notify::new()); + let server_test_done_clone = server_test_done.clone(); + let mut tcp_listener = TcpTunnelListener::new("tcp://0.0.0.0:55443".parse().unwrap()); + let s_task: ScopedTask<()> = tokio::spawn(async move { + tcp_listener.listen().await.unwrap(); + let tunnel = tcp_listener.accept().await.unwrap(); + s.run_with_tunnel(tunnel); + + let s_c = s + .rpc_client() + .scoped_client::>(1, 1, "test".to_string()); + let ret = s_c + .say_hello( + RpcController::default(), + SayHelloRequest { + name: "world".to_string(), + }, + ) + .await + .unwrap(); + assert_eq!(ret.greeting, "Hello Client world!"); + println!("server done, {:?}", ret); + + server_test_done_clone.notify_one(); + + s.wait().await; + }) + .into(); + + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let mut tcp_connector = TcpTunnelConnector::new("tcp://0.0.0.0:55443".parse().unwrap()); + let c_tunnel = tcp_connector.connect().await.unwrap(); + c.run_with_tunnel(c_tunnel); + + let c_c = c + .rpc_client() + .scoped_client::>(1, 1, "test".to_string()); + let ret = c_c + .say_hello( + RpcController::default(), + SayHelloRequest { + name: "world".to_string(), + }, + ) + .await + .unwrap(); + assert_eq!(ret.greeting, "Hello Server world!"); + println!("client done, {:?}", ret); + + server_test_done.notified().await; + drop(c); + s_task.await.unwrap(); +} diff --git a/rust/easytier/src/proto/web.proto b/rust/easytier/src/proto/web.proto new file mode 100644 index 0000000..a857245 --- /dev/null +++ b/rust/easytier/src/proto/web.proto @@ -0,0 +1,164 @@ +syntax = "proto3"; + +import "common.proto"; +import "peer_rpc.proto"; +import "cli.proto"; + +package web; + +enum NetworkingMethod { + PublicServer = 0; + Manual = 1; + Standalone = 2; +} + +message NetworkConfig { + optional string instance_id = 1; + + optional bool dhcp = 2; + optional string virtual_ipv4 = 3; + optional int32 network_length = 4; + optional string hostname = 5; + optional string network_name = 6; + optional string network_secret = 7; + optional NetworkingMethod networking_method = 8; + + optional string public_server_url = 9; + repeated string peer_urls = 10; + + repeated string proxy_cidrs = 11; + + optional bool enable_vpn_portal = 12; + optional int32 vpn_portal_listen_port = 13; + optional string vpn_portal_client_network_addr = 14; + optional int32 vpn_portal_client_network_len = 15; + + optional bool advanced_settings = 16; + + repeated string listener_urls = 17; + optional int32 rpc_port = 18; + optional bool latency_first = 19; + + optional string dev_name = 20; + + optional bool use_smoltcp = 21; + optional bool enable_kcp_proxy = 22; + optional bool disable_kcp_input = 23; + optional bool disable_p2p = 24; + optional bool bind_device = 25; + optional bool no_tun = 26; + + optional bool enable_exit_node = 27; + optional bool relay_all_peer_rpc = 28; + optional bool multi_thread = 29; + optional bool enable_relay_network_whitelist = 30; + repeated string relay_network_whitelist = 31; + optional bool enable_manual_routes = 32; + repeated string routes = 33; + repeated string exit_nodes = 34; + optional bool proxy_forward_by_system = 35; + optional bool disable_encryption = 36; + optional bool enable_socks5 = 37; + optional int32 socks5_port = 38; +} + +message MyNodeInfo { + common.Ipv4Inet virtual_ipv4 = 1; + string hostname = 2; + string version = 3; + peer_rpc.GetIpListResponse ips = 4; + common.StunInfo stun_info = 5; + repeated common.Url listeners = 6; + optional string vpn_portal_cfg = 7; +} + +message NetworkInstanceRunningInfo { + string dev_name = 1; + MyNodeInfo my_node_info = 2; + repeated string events = 3; + repeated cli.Route routes = 4; + repeated cli.PeerInfo peers = 5; + repeated cli.PeerRoutePair peer_route_pairs = 6; + bool running = 7; + optional string error_msg = 8; +} + +message NetworkInstanceRunningInfoMap { + map map = 1; +} + +message HeartbeatRequest { + common.UUID machine_id = 1; + common.UUID inst_id = 2; + string user_token = 3; + + string easytier_version = 4; + string report_time = 5; + string hostname = 6; + + repeated common.UUID running_network_instances = 7; +} + +message HeartbeatResponse { +} + +service WebServerService { + rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse) {} +} + +message ValidateConfigRequest { + NetworkConfig config = 1; +} + +message ValidateConfigResponse { + string toml_config = 1; +} + +message RunNetworkInstanceRequest { + common.UUID inst_id = 1; + NetworkConfig config = 2; +} + +message RunNetworkInstanceResponse { + common.UUID inst_id = 1; +} + +message RetainNetworkInstanceRequest { + repeated common.UUID inst_ids = 1; +} + +message RetainNetworkInstanceResponse { + repeated common.UUID remain_inst_ids = 1; +} + +message CollectNetworkInfoRequest { + repeated common.UUID inst_ids = 1; +} + +message CollectNetworkInfoResponse { + NetworkInstanceRunningInfoMap info = 1; +} + +message ListNetworkInstanceRequest { +} + +message ListNetworkInstanceResponse { + repeated common.UUID inst_ids = 1; +} + +message DeleteNetworkInstanceRequest { + repeated common.UUID inst_ids = 1; +} + +message DeleteNetworkInstanceResponse { + repeated common.UUID remain_inst_ids = 1; +} + +service WebClientService { + rpc ValidateConfig(ValidateConfigRequest) returns (ValidateConfigResponse) {} + rpc RunNetworkInstance(RunNetworkInstanceRequest) returns (RunNetworkInstanceResponse) {} + rpc RetainNetworkInstance(RetainNetworkInstanceRequest) returns (RetainNetworkInstanceResponse) {} + rpc CollectNetworkInfo(CollectNetworkInfoRequest) returns (CollectNetworkInfoResponse) {} + rpc ListNetworkInstance(ListNetworkInstanceRequest) returns (ListNetworkInstanceResponse) {} + rpc DeleteNetworkInstance(DeleteNetworkInstanceRequest) returns (DeleteNetworkInstanceResponse) {} +} diff --git a/rust/easytier/src/proto/web.rs b/rust/easytier/src/proto/web.rs new file mode 100644 index 0000000..a3254ec --- /dev/null +++ b/rust/easytier/src/proto/web.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/web.rs")); diff --git a/rust/easytier/src/tests/mod.rs b/rust/easytier/src/tests/mod.rs new file mode 100644 index 0000000..271dfed --- /dev/null +++ b/rust/easytier/src/tests/mod.rs @@ -0,0 +1,184 @@ +#[cfg(target_os = "linux")] +mod three_node; + +use crate::common::PeerId; +use crate::peers::peer_manager::PeerManager; + +pub fn get_guest_veth_name(net_ns: &str) -> &str { + Box::leak(format!("veth_{}_g", net_ns).into_boxed_str()) +} + +pub fn get_host_veth_name(net_ns: &str) -> &str { + Box::leak(format!("veth_{}_h", net_ns).into_boxed_str()) +} + +pub fn del_netns(name: &str) { + // del veth host + let _ = std::process::Command::new("ip") + .args(["link", "del", get_host_veth_name(name)]) + .output(); + + let _ = std::process::Command::new("ip") + .args(["netns", "del", name]) + .output(); +} + +pub fn create_netns(name: &str, ipv4: &str) { + // create netns + let _ = std::process::Command::new("ip") + .args(["netns", "add", name]) + .output() + .unwrap(); + + // set lo up + let _ = std::process::Command::new("ip") + .args(["netns", "exec", name, "ip", "link", "set", "lo", "up"]) + .output() + .unwrap(); + + let _ = std::process::Command::new("ip") + .args([ + "link", + "add", + get_host_veth_name(name), + "type", + "veth", + "peer", + "name", + get_guest_veth_name(name), + ]) + .output() + .unwrap(); + + let _ = std::process::Command::new("ip") + .args(["link", "set", get_guest_veth_name(name), "netns", name]) + .output() + .unwrap(); + + let _ = std::process::Command::new("ip") + .args([ + "netns", + "exec", + name, + "ip", + "link", + "set", + get_guest_veth_name(name), + "up", + ]) + .output() + .unwrap(); + + let _ = std::process::Command::new("ip") + .args(["link", "set", get_host_veth_name(name), "up"]) + .output() + .unwrap(); + + let _ = std::process::Command::new("ip") + .args([ + "netns", + "exec", + name, + "ip", + "addr", + "add", + ipv4, + "dev", + get_guest_veth_name(name), + ]) + .output() + .unwrap(); +} + +pub fn prepare_bridge(name: &str) { + // del bridge with brctl + let _ = std::process::Command::new("brctl") + .args(["delbr", name]) + .output(); + + // create new br + let _ = std::process::Command::new("brctl") + .args(["addbr", name]) + .output(); +} + +pub fn add_ns_to_bridge(br_name: &str, ns_name: &str) { + // use brctl to add ns to bridge + let _ = std::process::Command::new("brctl") + .args(["addif", br_name, get_host_veth_name(ns_name)]) + .output() + .unwrap(); + + // set bridge up + let _ = std::process::Command::new("ip") + .args(["link", "set", br_name, "up"]) + .output() + .unwrap(); +} + +pub fn enable_log() { + let filter = tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing::level_filters::LevelFilter::TRACE.into()) + .from_env() + .unwrap() + .add_directive("tarpc=error".parse().unwrap()); + tracing_subscriber::fmt::fmt() + .pretty() + .with_env_filter(filter) + .init(); +} + +fn check_route(ipv4: &str, dst_peer_id: PeerId, routes: Vec) { + let mut found = false; + for r in routes.iter() { + if r.ipv4_addr == Some(ipv4.parse().unwrap()) { + found = true; + assert_eq!(r.peer_id, dst_peer_id, "{:?}", routes); + } + } + assert!( + found, + "routes: {:?}, dst_peer_id: {}, ipv4: {}", + routes, dst_peer_id, ipv4 + ); +} + +async fn wait_proxy_route_appear( + mgr: &std::sync::Arc, + ipv4: &str, + dst_peer_id: PeerId, + proxy_cidr: &str, +) { + let now = std::time::Instant::now(); + loop { + for r in mgr.list_routes().await.iter() { + let r = r; + if r.proxy_cidrs.contains(&proxy_cidr.to_owned()) { + assert_eq!(r.peer_id, dst_peer_id); + assert_eq!(r.ipv4_addr, Some(ipv4.parse().unwrap())); + return; + } + } + if now.elapsed().as_secs() > 5 { + panic!("wait proxy route appear timeout"); + } + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } +} + +fn set_link_status(net_ns: &str, up: bool) { + let ret = std::process::Command::new("ip") + .args([ + "netns", + "exec", + net_ns, + "ip", + "link", + "set", + get_guest_veth_name(net_ns), + if up { "up" } else { "down" }, + ]) + .output() + .unwrap(); + tracing::info!("set link status: {:?}, net_ns: {}, up: {}", ret, net_ns, up); +} diff --git a/rust/easytier/src/tests/three_node.rs b/rust/easytier/src/tests/three_node.rs new file mode 100644 index 0000000..5fcbc1f --- /dev/null +++ b/rust/easytier/src/tests/three_node.rs @@ -0,0 +1,892 @@ +use core::panic; +use std::{ + sync::{atomic::AtomicU32, Arc}, + time::Duration, +}; + +use tokio::{net::UdpSocket, task::JoinSet}; + +use super::*; + +use crate::{ + common::{ + config::{ConfigLoader, NetworkIdentity, TomlConfigLoader}, + netns::{NetNS, ROOT_NETNS_NAME}, + }, + instance::instance::Instance, + proto::common::CompressionAlgoPb, + tunnel::{ + common::tests::wait_for_condition, ring::RingTunnelConnector, tcp::TcpTunnelConnector, + udp::UdpTunnelConnector, + }, +}; + +#[cfg(feature = "wireguard")] +use crate::{ + common::config::VpnPortalConfig, + tunnel::wireguard::{WgConfig, WgTunnelConnector}, + vpn_portal::wireguard::get_wg_config_for_portal, +}; + +pub fn prepare_linux_namespaces() { + del_netns("net_a"); + del_netns("net_b"); + del_netns("net_c"); + del_netns("net_d"); + + create_netns("net_a", "10.1.1.1/24"); + create_netns("net_b", "10.1.1.2/24"); + create_netns("net_c", "10.1.2.3/24"); + create_netns("net_d", "10.1.2.4/24"); + + prepare_bridge("br_a"); + prepare_bridge("br_b"); + + add_ns_to_bridge("br_a", "net_a"); + add_ns_to_bridge("br_a", "net_b"); + add_ns_to_bridge("br_b", "net_c"); + add_ns_to_bridge("br_b", "net_d"); +} + +pub fn get_inst_config(inst_name: &str, ns: Option<&str>, ipv4: &str) -> TomlConfigLoader { + let config = TomlConfigLoader::default(); + config.set_inst_name(inst_name.to_owned()); + config.set_netns(ns.map(|s| s.to_owned())); + config.set_ipv4(Some(ipv4.parse().unwrap())); + config.set_listeners(vec![ + "tcp://0.0.0.0:11010".parse().unwrap(), + "udp://0.0.0.0:11010".parse().unwrap(), + "wg://0.0.0.0:11011".parse().unwrap(), + "ws://0.0.0.0:11011".parse().unwrap(), + "wss://0.0.0.0:11012".parse().unwrap(), + ]); + config.set_socks5_portal(Some("socks5://0.0.0.0:12345".parse().unwrap())); + config +} + +pub async fn init_three_node(proto: &str) -> Vec { + init_three_node_ex(proto, |cfg| cfg, false).await +} + +pub async fn init_three_node_ex TomlConfigLoader>( + proto: &str, + cfg_cb: F, + use_public_server: bool, +) -> Vec { + prepare_linux_namespaces(); + + let mut inst1 = Instance::new(cfg_cb(get_inst_config( + "inst1", + Some("net_a"), + "10.144.144.1", + ))); + let mut inst2 = Instance::new(cfg_cb(get_inst_config( + "inst2", + Some("net_b"), + "10.144.144.2", + ))); + let mut inst3 = Instance::new(cfg_cb(get_inst_config( + "inst3", + Some("net_c"), + "10.144.144.3", + ))); + + inst1.run().await.unwrap(); + inst2.run().await.unwrap(); + inst3.run().await.unwrap(); + + if proto == "tcp" { + inst1 + .get_conn_manager() + .add_connector(TcpTunnelConnector::new( + "tcp://10.1.1.2:11010".parse().unwrap(), + )); + } else if proto == "udp" { + inst1 + .get_conn_manager() + .add_connector(UdpTunnelConnector::new( + "udp://10.1.1.2:11010".parse().unwrap(), + )); + } else if proto == "wg" { + #[cfg(feature = "wireguard")] + inst1 + .get_conn_manager() + .add_connector(WgTunnelConnector::new( + "wg://10.1.1.2:11011".parse().unwrap(), + WgConfig::new_from_network_identity( + &inst2.get_global_ctx().get_network_identity().network_name, + &inst2 + .get_global_ctx() + .get_network_identity() + .network_secret + .unwrap_or_default(), + ), + )); + } else if proto == "ws" { + #[cfg(feature = "websocket")] + inst1 + .get_conn_manager() + .add_connector(crate::tunnel::websocket::WSTunnelConnector::new( + "ws://10.1.1.2:11011".parse().unwrap(), + )); + } else if proto == "wss" { + #[cfg(feature = "websocket")] + inst1 + .get_conn_manager() + .add_connector(crate::tunnel::websocket::WSTunnelConnector::new( + "wss://10.1.1.2:11012".parse().unwrap(), + )); + } + + inst3 + .get_conn_manager() + .add_connector(RingTunnelConnector::new( + format!("ring://{}", inst2.id()).parse().unwrap(), + )); + + // wait inst2 have two route. + wait_for_condition( + || async { + if !use_public_server { + inst2.get_peer_manager().list_routes().await.len() == 2 + } else { + inst2 + .get_peer_manager() + .get_foreign_network_manager() + .list_foreign_networks() + .await + .foreign_networks + .len() + == 1 + } + }, + Duration::from_secs(5), + ) + .await; + + wait_for_condition( + || async { + let routes = inst1.get_peer_manager().list_routes().await; + println!("routes: {:?}", routes); + routes.len() == 2 + }, + Duration::from_secs(5), + ) + .await; + + vec![inst1, inst2, inst3] +} + +async fn ping_test(from_netns: &str, target_ip: &str, payload_size: Option) -> bool { + let _g = NetNS::new(Some(ROOT_NETNS_NAME.to_owned())).guard(); + let code = tokio::process::Command::new("ip") + .args(&[ + "netns", + "exec", + from_netns, + "ping", + "-c", + "1", + "-s", + payload_size.unwrap_or(56).to_string().as_str(), + "-W", + "1", + target_ip.to_string().as_str(), + ]) + .status() + .await + .unwrap(); + code.code().unwrap() == 0 +} + +#[rstest::rstest] +#[tokio::test] +#[serial_test::serial] +pub async fn basic_three_node_test(#[values("tcp", "udp", "wg", "ws", "wss")] proto: &str) { + let insts = init_three_node(proto).await; + + check_route( + "10.144.144.2/24", + insts[1].peer_id(), + insts[0].get_peer_manager().list_routes().await, + ); + + check_route( + "10.144.144.3/24", + insts[2].peer_id(), + insts[0].get_peer_manager().list_routes().await, + ); + + wait_for_condition( + || async { ping_test("net_c", "10.144.144.1", None).await }, + Duration::from_secs(5000), + ) + .await; +} + +async fn subnet_proxy_test_udp() { + use crate::tunnel::{common::tests::_tunnel_pingpong_netns, udp::UdpTunnelListener}; + use rand::Rng; + + let udp_listener = UdpTunnelListener::new("udp://10.1.2.4:22233".parse().unwrap()); + let udp_connector = UdpTunnelConnector::new("udp://10.1.2.4:22233".parse().unwrap()); + + // NOTE: this should not excced udp tunnel max buffer size + let mut buf = vec![0; 20 * 1024]; + rand::thread_rng().fill(&mut buf[..]); + + _tunnel_pingpong_netns( + udp_listener, + udp_connector, + NetNS::new(Some("net_d".into())), + NetNS::new(Some("net_a".into())), + buf, + ) + .await; + + // no fragment + let udp_listener = UdpTunnelListener::new("udp://10.1.2.4:22233".parse().unwrap()); + let udp_connector = UdpTunnelConnector::new("udp://10.1.2.4:22233".parse().unwrap()); + + let mut buf = vec![0; 1 * 1024]; + rand::thread_rng().fill(&mut buf[..]); + + _tunnel_pingpong_netns( + udp_listener, + udp_connector, + NetNS::new(Some("net_d".into())), + NetNS::new(Some("net_a".into())), + buf, + ) + .await; + + // connect to virtual ip (no tun mode) + + let udp_listener = UdpTunnelListener::new("udp://0.0.0.0:22234".parse().unwrap()); + let udp_connector = UdpTunnelConnector::new("udp://10.144.144.3:22234".parse().unwrap()); + // NOTE: this should not excced udp tunnel max buffer size + let mut buf = vec![0; 20 * 1024]; + rand::thread_rng().fill(&mut buf[..]); + + _tunnel_pingpong_netns( + udp_listener, + udp_connector, + NetNS::new(Some("net_c".into())), + NetNS::new(Some("net_a".into())), + buf, + ) + .await; + + // no fragment + let udp_listener = UdpTunnelListener::new("udp://0.0.0.0:22235".parse().unwrap()); + let udp_connector = UdpTunnelConnector::new("udp://10.144.144.3:22235".parse().unwrap()); + + let mut buf = vec![0; 1 * 1024]; + rand::thread_rng().fill(&mut buf[..]); + + _tunnel_pingpong_netns( + udp_listener, + udp_connector, + NetNS::new(Some("net_c".into())), + NetNS::new(Some("net_a".into())), + buf, + ) + .await; +} + +async fn subnet_proxy_test_tcp() { + use crate::tunnel::{common::tests::_tunnel_pingpong_netns, tcp::TcpTunnelListener}; + use rand::Rng; + + let tcp_listener = TcpTunnelListener::new("tcp://10.1.2.4:22223".parse().unwrap()); + let tcp_connector = TcpTunnelConnector::new("tcp://10.1.2.4:22223".parse().unwrap()); + + let mut buf = vec![0; 32]; + rand::thread_rng().fill(&mut buf[..]); + + _tunnel_pingpong_netns( + tcp_listener, + tcp_connector, + NetNS::new(Some("net_d".into())), + NetNS::new(Some("net_a".into())), + buf, + ) + .await; + + // connect to virtual ip (no tun mode) + let tcp_listener = TcpTunnelListener::new("tcp://0.0.0.0:22223".parse().unwrap()); + let tcp_connector = TcpTunnelConnector::new("tcp://10.144.144.3:22223".parse().unwrap()); + + let mut buf = vec![0; 32]; + rand::thread_rng().fill(&mut buf[..]); + + _tunnel_pingpong_netns( + tcp_listener, + tcp_connector, + NetNS::new(Some("net_c".into())), + NetNS::new(Some("net_a".into())), + buf, + ) + .await; +} + +async fn subnet_proxy_test_icmp() { + wait_for_condition( + || async { ping_test("net_a", "10.1.2.4", None).await }, + Duration::from_secs(5), + ) + .await; + + wait_for_condition( + || async { ping_test("net_a", "10.1.2.4", Some(5 * 1024)).await }, + Duration::from_secs(5), + ) + .await; + + // connect to virtual ip (no tun mode) + wait_for_condition( + || async { ping_test("net_a", "10.144.144.3", None).await }, + Duration::from_secs(5), + ) + .await; + + wait_for_condition( + || async { ping_test("net_a", "10.144.144.3", Some(5 * 1024)).await }, + Duration::from_secs(5), + ) + .await; +} + +#[rstest::rstest] +#[tokio::test] +#[serial_test::serial] +pub async fn subnet_proxy_three_node_test( + #[values("tcp", "udp", "wg")] proto: &str, + #[values(true, false)] no_tun: bool, + #[values(true, false)] relay_by_public_server: bool, + #[values(true, false)] enable_kcp_proxy: bool, + #[values(true, false)] disable_kcp_input: bool, + #[values(true, false)] dst_enable_kcp_proxy: bool, +) { + let insts = init_three_node_ex( + proto, + |cfg| { + if cfg.get_inst_name() == "inst3" { + let mut flags = cfg.get_flags(); + flags.no_tun = no_tun; + flags.disable_kcp_input = disable_kcp_input; + flags.enable_kcp_proxy = dst_enable_kcp_proxy; + cfg.set_flags(flags); + cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap()); + } + + if cfg.get_inst_name() == "inst2" && relay_by_public_server { + cfg.set_network_identity(NetworkIdentity::new( + "public".to_string(), + "public".to_string(), + )); + } + + if cfg.get_inst_name() == "inst1" && enable_kcp_proxy { + let mut flags = cfg.get_flags(); + flags.enable_kcp_proxy = true; + cfg.set_flags(flags); + } + + cfg + }, + relay_by_public_server, + ) + .await; + + assert_eq!(insts[2].get_global_ctx().get_proxy_cidrs().len(), 1); + + wait_proxy_route_appear( + &insts[0].get_peer_manager(), + "10.144.144.3/24", + insts[2].peer_id(), + "10.1.2.0/24", + ) + .await; + + subnet_proxy_test_icmp().await; + subnet_proxy_test_tcp().await; + subnet_proxy_test_udp().await; +} + +#[rstest::rstest] +#[tokio::test] +#[serial_test::serial] +pub async fn data_compress( + #[values(true, false)] inst1_compress: bool, + #[values(true, false)] inst2_compress: bool, +) { + let _insts = init_three_node_ex( + "udp", + |cfg| { + if cfg.get_inst_name() == "inst1" && inst1_compress { + let mut flags = cfg.get_flags(); + flags.data_compress_algo = CompressionAlgoPb::Zstd.into(); + cfg.set_flags(flags); + } + + if cfg.get_inst_name() == "inst3" && inst2_compress { + let mut flags = cfg.get_flags(); + flags.data_compress_algo = CompressionAlgoPb::Zstd.into(); + cfg.set_flags(flags); + } + + cfg + }, + false, + ) + .await; + + wait_for_condition( + || async { ping_test("net_a", "10.144.144.3", None).await }, + Duration::from_secs(5), + ) + .await; + + wait_for_condition( + || async { ping_test("net_a", "10.144.144.3", Some(5 * 1024)).await }, + Duration::from_secs(5), + ) + .await; +} + +#[cfg(feature = "wireguard")] +#[rstest::rstest] +#[tokio::test] +#[serial_test::serial] +pub async fn proxy_three_node_disconnect_test(#[values("tcp", "wg")] proto: &str) { + use crate::{ + common::scoped_task::ScopedTask, + tunnel::wireguard::{WgConfig, WgTunnelConnector}, + }; + + let insts = init_three_node(proto).await; + let mut inst4 = Instance::new(get_inst_config("inst4", Some("net_d"), "10.144.144.4")); + if proto == "tcp" { + inst4 + .get_conn_manager() + .add_connector(TcpTunnelConnector::new( + "tcp://10.1.2.3:11010".parse().unwrap(), + )); + } else if proto == "wg" { + inst4 + .get_conn_manager() + .add_connector(WgTunnelConnector::new( + "wg://10.1.2.3:11011".parse().unwrap(), + WgConfig::new_from_network_identity( + &inst4.get_global_ctx().get_network_identity().network_name, + &inst4 + .get_global_ctx() + .get_network_identity() + .network_secret + .unwrap_or_default(), + ), + )); + } else { + unreachable!("not support"); + } + inst4.run().await.unwrap(); + + tracing::info!("inst1 peer id: {:?}", insts[0].peer_id()); + tracing::info!("inst2 peer id: {:?}", insts[1].peer_id()); + tracing::info!("inst3 peer id: {:?}", insts[2].peer_id()); + tracing::info!("inst4 peer id: {:?}", inst4.peer_id()); + + let task = tokio::spawn(async move { + for _ in 1..=2 { + // inst4 should be in inst1's route list + wait_for_condition( + || async { + insts[0] + .get_peer_manager() + .list_routes() + .await + .iter() + .find(|r| r.peer_id == inst4.peer_id()) + .is_some() + }, + Duration::from_secs(8), + ) + .await; + + set_link_status("net_d", false); + let _t = ScopedTask::from(tokio::spawn(async move { + // do some ping in net_a to trigger net_c pingpong + loop { + ping_test("net_a", "10.144.144.4", Some(1)).await; + } + })); + wait_for_condition( + || async { + let ret = insts[2] + .get_peer_manager() + .get_peer_map() + .list_peers_with_conn() + .await + .iter() + .find(|r| **r == inst4.peer_id()) + .is_none(); + if !ret { + println!( + "conn info: {:?}", + insts[2] + .get_peer_manager() + .get_peer_map() + .list_peer_conns(inst4.peer_id()) + .await + ); + } + ret + }, + // 0 down, assume last packet is recv in -0.01 + // [2, 7) send ping + // [4, 9) ping fail and close connection + Duration::from_secs(11), + ) + .await; + + wait_for_condition( + || async { + insts[0] + .get_peer_manager() + .list_routes() + .await + .iter() + .find(|r| r.peer_id == inst4.peer_id()) + .is_none() + }, + Duration::from_secs(7), + ) + .await; + + set_link_status("net_d", true); + } + }); + + let (ret,) = tokio::join!(task); + assert!(ret.is_ok()); +} + +#[tokio::test] +#[serial_test::serial] +pub async fn udp_broadcast_test() { + let _insts = init_three_node("tcp").await; + + let udp_broadcast_responder = |net_ns: NetNS, counter: Arc| async move { + let _g = net_ns.guard(); + let socket: UdpSocket = UdpSocket::bind("0.0.0.0:22111").await.unwrap(); + socket.set_broadcast(true).unwrap(); + + println!("Awaiting responses..."); // self.recv_buff is a [u8; 8092] + let mut recv_buff = [0; 8092]; + while let Ok((n, addr)) = socket.recv_from(&mut recv_buff).await { + println!("{} bytes response from {:?}", n, addr); + counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + // Remaining code not directly relevant to the question + } + }; + + let mut tasks = JoinSet::new(); + let counter = Arc::new(AtomicU32::new(0)); + tasks.spawn(udp_broadcast_responder( + NetNS::new(Some("net_b".into())), + counter.clone(), + )); + tasks.spawn(udp_broadcast_responder( + NetNS::new(Some("net_c".into())), + counter.clone(), + )); + + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + // send broadcast + let net_ns = NetNS::new(Some("net_a".into())); + let _g = net_ns.guard(); + let socket: UdpSocket = UdpSocket::bind("0.0.0.0:0").await.unwrap(); + socket.set_broadcast(true).unwrap(); + // socket.connect(("10.144.144.255", 22111)).await.unwrap(); + let call: Vec = vec![1; 1024]; + println!("Sending call, {} bytes", call.len()); + match socket.send_to(&call, "10.144.144.255:22111").await { + Err(e) => panic!("Error sending call: {:?}", e), + _ => {} + } + + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + assert_eq!(counter.load(std::sync::atomic::Ordering::Relaxed), 2); +} + +#[tokio::test] +#[serial_test::serial] +pub async fn foreign_network_forward_nic_data() { + prepare_linux_namespaces(); + + let center_node_config = get_inst_config("inst1", Some("net_a"), "10.144.144.1"); + center_node_config + .set_network_identity(NetworkIdentity::new("center".to_string(), "".to_string())); + let mut center_inst = Instance::new(center_node_config); + + let mut inst1 = Instance::new(get_inst_config("inst1", Some("net_b"), "10.144.145.1")); + let mut inst2 = Instance::new(get_inst_config("inst2", Some("net_c"), "10.144.145.2")); + + center_inst.run().await.unwrap(); + inst1.run().await.unwrap(); + inst2.run().await.unwrap(); + + assert_ne!(inst1.id(), center_inst.id()); + assert_ne!(inst2.id(), center_inst.id()); + + inst1 + .get_conn_manager() + .add_connector(RingTunnelConnector::new( + format!("ring://{}", center_inst.id()).parse().unwrap(), + )); + + inst2 + .get_conn_manager() + .add_connector(RingTunnelConnector::new( + format!("ring://{}", center_inst.id()).parse().unwrap(), + )); + + wait_for_condition( + || async { + inst1.get_peer_manager().list_routes().await.len() == 2 + && inst2.get_peer_manager().list_routes().await.len() == 2 + }, + Duration::from_secs(5), + ) + .await; + + wait_for_condition( + || async { ping_test("net_b", "10.144.145.2", None).await }, + Duration::from_secs(5), + ) + .await; +} + +use std::{net::SocketAddr, str::FromStr}; + +use defguard_wireguard_rs::{ + host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration, WGApi, WireguardInterfaceApi, +}; + +fn run_wireguard_client( + endpoint: SocketAddr, + peer_public_key: Key, + client_private_key: Key, + allowed_ips: Vec, + client_ip: String, +) -> Result<(), Box> { + // Create new API object for interface + let ifname: String = if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") { + "wg0".into() + } else { + "utun3".into() + }; + let wgapi = WGApi::new(ifname.clone(), false)?; + + // create interface + wgapi.create_interface()?; + + // Peer secret key + let mut peer = Peer::new(peer_public_key.clone()); + + tracing::info!("endpoint"); + // Peer endpoint and interval + peer.endpoint = Some(endpoint); + peer.persistent_keepalive_interval = Some(1); + for ip in allowed_ips { + peer.allowed_ips.push(IpAddrMask::from_str(ip.as_str())?); + } + + // interface configuration + let interface_config = InterfaceConfiguration { + name: ifname.clone(), + prvkey: client_private_key.to_string(), + address: client_ip, + port: 12345, + peers: vec![peer], + }; + + #[cfg(not(windows))] + wgapi.configure_interface(&interface_config)?; + #[cfg(windows)] + wgapi.configure_interface(&interface_config, &[])?; + wgapi.configure_peer_routing(&interface_config.peers)?; + Ok(()) +} + +#[cfg(feature = "wireguard")] +#[tokio::test] +#[serial_test::serial] +pub async fn wireguard_vpn_portal() { + let mut insts = init_three_node("tcp").await; + let net_ns = NetNS::new(Some("net_d".into())); + let _g = net_ns.guard(); + insts[2] + .get_global_ctx() + .config + .set_vpn_portal_config(VpnPortalConfig { + wireguard_listen: "0.0.0.0:22121".parse().unwrap(), + client_cidr: "10.14.14.0/24".parse().unwrap(), + }); + insts[2].run_vpn_portal().await.unwrap(); + + let net_ns = NetNS::new(Some("net_d".into())); + let _g = net_ns.guard(); + let wg_cfg = get_wg_config_for_portal(&insts[2].get_global_ctx().get_network_identity()); + run_wireguard_client( + "10.1.2.3:22121".parse().unwrap(), + Key::try_from(wg_cfg.my_public_key()).unwrap(), + Key::try_from(wg_cfg.peer_secret_key()).unwrap(), + vec!["10.14.14.0/24".to_string(), "10.144.144.0/24".to_string()], + "10.14.14.2".to_string(), + ) + .unwrap(); + + // ping other node in network + wait_for_condition( + || async { ping_test("net_d", "10.144.144.1", None).await }, + Duration::from_secs(5), + ) + .await; + wait_for_condition( + || async { ping_test("net_d", "10.144.144.2", None).await }, + Duration::from_secs(5), + ) + .await; + + // ping portal node + wait_for_condition( + || async { ping_test("net_d", "10.144.144.3", None).await }, + Duration::from_secs(5), + ) + .await; +} + +#[cfg(feature = "wireguard")] +#[rstest::rstest] +#[tokio::test] +#[serial_test::serial] +pub async fn socks5_vpn_portal(#[values("10.144.144.1", "10.144.144.3")] dst_addr: &str) { + use rand::Rng as _; + use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::{TcpListener, TcpStream}, + }; + use tokio_socks::tcp::socks5::Socks5Stream; + + let _insts = init_three_node("tcp").await; + + let mut buf = vec![0u8; 1024]; + rand::thread_rng().fill(&mut buf[..]); + + let buf_clone = buf.clone(); + let dst_addr_clone = dst_addr.to_owned(); + let task = tokio::spawn(async move { + let net_ns = if dst_addr_clone == "10.144.144.1" { + NetNS::new(Some("net_a".into())) + } else { + NetNS::new(Some("net_c".into())) + }; + let _g = net_ns.guard(); + + let socket = TcpListener::bind("0.0.0.0:22222").await.unwrap(); + let (mut st, addr) = socket.accept().await.unwrap(); + + if dst_addr_clone == "10.144.144.3" { + assert_eq!(addr.ip().to_string(), "10.144.144.1".to_string()); + } else { + assert_eq!(addr.ip().to_string(), "127.0.0.1".to_string()); + } + + let rbuf = &mut [0u8; 1024]; + st.read_exact(rbuf).await.unwrap(); + assert_eq!(rbuf, buf_clone.as_slice()); + }); + + let net_ns = NetNS::new(Some("net_a".into())); + let _g = net_ns.guard(); + + println!("connect to socks5 portal"); + let stream = TcpStream::connect("127.0.0.1:12345").await.unwrap(); + println!("connect to socks5 portal done"); + + stream.set_nodelay(true).unwrap(); + let mut conn = Socks5Stream::connect_with_socket(stream, format!("{}:22222", dst_addr)) + .await + .unwrap(); + + conn.write_all(&buf).await.unwrap(); + drop(conn); + + tokio::join!(task).0.unwrap(); +} + +#[rstest::rstest] +#[tokio::test] +#[serial_test::serial] +pub async fn manual_reconnector(#[values(true, false)] is_foreign: bool) { + prepare_linux_namespaces(); + + let center_node_config = get_inst_config("inst1", Some("net_a"), "10.144.144.1"); + if is_foreign { + center_node_config + .set_network_identity(NetworkIdentity::new("center".to_string(), "".to_string())); + } + let mut center_inst = Instance::new(center_node_config); + + let inst1_config = get_inst_config("inst1", Some("net_b"), "10.144.145.1"); + inst1_config.set_listeners(vec![]); + let mut inst1 = Instance::new(inst1_config); + + let mut inst2 = Instance::new(get_inst_config("inst2", Some("net_c"), "10.144.145.2")); + + center_inst.run().await.unwrap(); + inst1.run().await.unwrap(); + inst2.run().await.unwrap(); + + assert_ne!(inst1.id(), center_inst.id()); + assert_ne!(inst2.id(), center_inst.id()); + + inst1 + .get_conn_manager() + .add_connector(RingTunnelConnector::new( + format!("ring://{}", center_inst.id()).parse().unwrap(), + )); + + inst2 + .get_conn_manager() + .add_connector(RingTunnelConnector::new( + format!("ring://{}", center_inst.id()).parse().unwrap(), + )); + + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + + let peer_map = if !is_foreign { + inst1.get_peer_manager().get_peer_map() + } else { + inst1 + .get_peer_manager() + .get_foreign_network_client() + .get_peer_map() + }; + + let conns = peer_map + .list_peer_conns(center_inst.peer_id()) + .await + .unwrap(); + + assert!(conns.len() >= 1); + + wait_for_condition( + || async { ping_test("net_b", "10.144.145.2", None).await }, + Duration::from_secs(5), + ) + .await; +} diff --git a/rust/easytier/src/tunnel/buf.rs b/rust/easytier/src/tunnel/buf.rs new file mode 100644 index 0000000..0136309 --- /dev/null +++ b/rust/easytier/src/tunnel/buf.rs @@ -0,0 +1,92 @@ +use std::collections::VecDeque; +use std::io::IoSlice; + +use bytes::{Buf, BufMut, Bytes, BytesMut}; + +pub(crate) struct BufList { + bufs: VecDeque, +} + +impl BufList { + pub(crate) fn new() -> BufList { + BufList { + bufs: VecDeque::new(), + } + } + + #[inline] + pub(crate) fn push(&mut self, buf: T) { + debug_assert!(buf.has_remaining()); + self.bufs.push_back(buf); + } + + #[inline] + pub(crate) fn bufs_cnt(&self) -> usize { + self.bufs.len() + } +} + +impl Buf for BufList { + #[inline] + fn remaining(&self) -> usize { + self.bufs.iter().map(|buf| buf.remaining()).sum() + } + + #[inline] + fn chunk(&self) -> &[u8] { + self.bufs.front().map(Buf::chunk).unwrap_or_default() + } + + #[inline] + fn advance(&mut self, mut cnt: usize) { + while cnt > 0 { + { + let front = &mut self.bufs[0]; + let rem = front.remaining(); + if rem > cnt { + front.advance(cnt); + return; + } else { + front.advance(rem); + cnt -= rem; + } + } + self.bufs.pop_front(); + } + } + + #[inline] + fn chunks_vectored<'t>(&'t self, dst: &mut [IoSlice<'t>]) -> usize { + if dst.is_empty() { + return 0; + } + let mut vecs = 0; + for buf in &self.bufs { + vecs += buf.chunks_vectored(&mut dst[vecs..]); + if vecs == dst.len() { + break; + } + } + vecs + } + + #[inline] + fn copy_to_bytes(&mut self, len: usize) -> Bytes { + // Our inner buffer may have an optimized version of copy_to_bytes, and if the whole + // request can be fulfilled by the front buffer, we can take advantage. + match self.bufs.front_mut() { + Some(front) if front.remaining() == len => { + let b = front.copy_to_bytes(len); + self.bufs.pop_front(); + b + } + Some(front) if front.remaining() > len => front.copy_to_bytes(len), + _ => { + assert!(len <= self.remaining(), "`len` greater than remaining"); + let mut bm = BytesMut::with_capacity(len); + bm.put(self.take(len)); + bm.freeze() + } + } + } +} diff --git a/rust/easytier/src/tunnel/common.rs b/rust/easytier/src/tunnel/common.rs new file mode 100644 index 0000000..cc5ee29 --- /dev/null +++ b/rust/easytier/src/tunnel/common.rs @@ -0,0 +1,634 @@ +use std::{ + any::Any, + net::{IpAddr, SocketAddr}, + pin::Pin, + sync::{Arc, Mutex}, + task::{ready, Poll}, +}; + +use futures::{stream::FuturesUnordered, Future, Sink, Stream}; +use network_interface::NetworkInterfaceConfig as _; +use pin_project_lite::pin_project; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use tokio_stream::StreamExt; +use tokio_util::io::poll_write_buf; +use zerocopy::FromBytes as _; + +use super::TunnelInfo; + +use crate::tunnel::packet_def::{ZCPacket, PEER_MANAGER_HEADER_SIZE}; + +use super::{ + buf::BufList, + packet_def::{TCPTunnelHeader, ZCPacketType, TCP_TUNNEL_HEADER_SIZE}, + SinkItem, StreamItem, Tunnel, TunnelError, ZCPacketSink, ZCPacketStream, +}; + +pub struct TunnelWrapper { + reader: Arc>>, + writer: Arc>>, + info: Option, + associate_data: Option>, +} + +impl TunnelWrapper { + pub fn new(reader: R, writer: W, info: Option) -> Self { + Self::new_with_associate_data(reader, writer, info, None) + } + + pub fn new_with_associate_data( + reader: R, + writer: W, + info: Option, + associate_data: Option>, + ) -> Self { + TunnelWrapper { + reader: Arc::new(Mutex::new(Some(reader))), + writer: Arc::new(Mutex::new(Some(writer))), + info, + associate_data, + } + } +} + +impl Tunnel for TunnelWrapper +where + R: ZCPacketStream + Send + 'static, + W: ZCPacketSink + Send + 'static, +{ + fn split(&self) -> (Pin>, Pin>) { + let reader = self.reader.lock().unwrap().take().unwrap(); + let writer = self.writer.lock().unwrap().take().unwrap(); + (Box::pin(reader), Box::pin(writer)) + } + + fn info(&self) -> Option { + self.info.clone() + } +} + +// a length delimited codec for async reader +pin_project! { + pub struct FramedReader { + #[pin] + reader: R, + buf: BytesMut, + max_packet_size: usize, + associate_data: Option>, + error: Option, + } +} + +impl FramedReader { + pub fn new(reader: R, max_packet_size: usize) -> Self { + Self::new_with_associate_data(reader, max_packet_size, None) + } + + pub fn new_with_associate_data( + reader: R, + max_packet_size: usize, + associate_data: Option>, + ) -> Self { + FramedReader { + reader, + buf: BytesMut::with_capacity(max_packet_size), + max_packet_size, + associate_data, + error: None, + } + } + + fn extract_one_packet( + buf: &mut BytesMut, + max_packet_size: usize, + ) -> Option> { + if buf.len() < TCP_TUNNEL_HEADER_SIZE { + // header is not complete + return None; + } + + let header = TCPTunnelHeader::ref_from_prefix(&buf[..]).unwrap(); + let body_len = header.len.get() as usize; + if body_len > max_packet_size { + // body is too long + return Some(Err(TunnelError::InvalidPacket("body too long".to_string()))); + } + + if buf.len() < TCP_TUNNEL_HEADER_SIZE + body_len { + // body is not complete + return None; + } + + // extract one packet + let packet_buf = buf.split_to(TCP_TUNNEL_HEADER_SIZE + body_len); + Some(Ok(ZCPacket::new_from_buf(packet_buf, ZCPacketType::TCP))) + } +} + +impl Stream for FramedReader +where + R: AsyncRead + Send + 'static + Unpin, +{ + type Item = StreamItem; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let mut self_mut = self.project(); + + loop { + if let Some(e) = self_mut.error.as_ref() { + tracing::warn!("poll_next on a failed FramedReader, {:?}", e); + return Poll::Ready(None); + } + + while let Some(packet) = + Self::extract_one_packet(self_mut.buf, *self_mut.max_packet_size) + { + if let Err(TunnelError::InvalidPacket(msg)) = packet.as_ref() { + self_mut + .error + .replace(TunnelError::InvalidPacket(msg.clone())); + } + return Poll::Ready(Some(packet)); + } + + reserve_buf( + &mut self_mut.buf, + *self_mut.max_packet_size, + *self_mut.max_packet_size * 32, + ); + + let cap = self_mut.buf.capacity() - self_mut.buf.len(); + let buf = self_mut.buf.chunk_mut().as_mut_ptr(); + let buf = unsafe { std::slice::from_raw_parts_mut(buf, cap) }; + let mut buf = ReadBuf::new(buf); + + let ret = ready!(self_mut.reader.as_mut().poll_read(cx, &mut buf)); + let len = buf.filled().len(); + unsafe { self_mut.buf.advance_mut(len) }; + + match ret { + Ok(_) => { + if len == 0 { + return Poll::Ready(None); + } + } + Err(e) => { + return Poll::Ready(Some(Err(TunnelError::IOError(e)))); + } + } + } + } +} + +pub trait ZCPacketToBytes { + fn into_bytes(&self, zc_packet: ZCPacket) -> Result; +} + +pub struct TcpZCPacketToBytes; +impl ZCPacketToBytes for TcpZCPacketToBytes { + fn into_bytes(&self, item: ZCPacket) -> Result { + let mut item = item.convert_type(ZCPacketType::TCP); + + let tcp_len = PEER_MANAGER_HEADER_SIZE + item.payload_len(); + let Some(header) = item.mut_tcp_tunnel_header() else { + return Err(TunnelError::InvalidPacket("packet too short".to_string())); + }; + header.len.set(tcp_len.try_into().unwrap()); + + Ok(item.into_bytes()) + } +} + +pin_project! { + pub struct FramedWriter { + #[pin] + writer: W, + sending_bufs: BufList, + associate_data: Option>, + + converter: C, + } +} + +impl FramedWriter { + fn max_buffer_count(&self) -> usize { + 64 + } +} + +impl FramedWriter { + pub fn new(writer: W) -> Self { + Self::new_with_associate_data(writer, None) + } + + pub fn new_with_associate_data( + writer: W, + associate_data: Option>, + ) -> Self { + FramedWriter { + writer, + sending_bufs: BufList::new(), + associate_data, + converter: TcpZCPacketToBytes {}, + } + } +} + +impl FramedWriter { + pub fn new_with_converter(writer: W, converter: C) -> Self { + Self::new_with_converter_and_associate_data(writer, converter, None) + } + + pub fn new_with_converter_and_associate_data( + writer: W, + converter: C, + associate_data: Option>, + ) -> Self { + FramedWriter { + writer, + sending_bufs: BufList::new(), + associate_data, + converter, + } + } +} + +impl Sink for FramedWriter +where + W: AsyncWrite + Send + 'static, + C: ZCPacketToBytes + Send + 'static, +{ + type Error = TunnelError; + + fn poll_ready( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let max_buffer_count = self.max_buffer_count(); + if self.sending_bufs.bufs_cnt() >= max_buffer_count { + self.as_mut().poll_flush(cx) + } else { + tracing::trace!(bufs_cnt = self.sending_bufs.bufs_cnt(), "ready to send"); + Poll::Ready(Ok(())) + } + } + + fn start_send(self: Pin<&mut Self>, item: ZCPacket) -> Result<(), Self::Error> { + let pinned = self.project(); + pinned.sending_bufs.push(pinned.converter.into_bytes(item)?); + + Ok(()) + } + + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let mut pinned = self.project(); + let mut remaining = pinned.sending_bufs.remaining(); + while remaining != 0 { + let n = ready!(poll_write_buf( + pinned.writer.as_mut(), + cx, + pinned.sending_bufs + ))?; + if n == 0 { + return Poll::Ready(Err(TunnelError::IOError(std::io::Error::new( + std::io::ErrorKind::WriteZero, + "failed to \ + write frame to transport", + )))); + } + remaining -= n; + } + + tracing::trace!(?remaining, "flushed"); + + // Try flushing the underlying IO + ready!(pinned.writer.poll_flush(cx))?; + + Poll::Ready(Ok(())) + } + + fn poll_close( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + ready!(self.as_mut().poll_flush(cx))?; + ready!(self.project().writer.poll_shutdown(cx))?; + + Poll::Ready(Ok(())) + } +} + +pub(crate) fn get_interface_name_by_ip(local_ip: &IpAddr) -> Option { + if local_ip.is_unspecified() || local_ip.is_multicast() { + return None; + } + let ifaces = network_interface::NetworkInterface::show().ok()?; + for iface in ifaces { + for addr in iface.addr { + if addr.ip() == *local_ip { + return Some(iface.name); + } + } + } + + tracing::error!(?local_ip, "can not find interface name by ip"); + None +} + +pub(crate) fn setup_sokcet2_ext( + socket2_socket: &socket2::Socket, + bind_addr: &SocketAddr, + #[allow(unused_variables)] bind_dev: Option, +) -> Result<(), TunnelError> { + #[cfg(target_os = "windows")] + { + let is_udp = matches!(socket2_socket.r#type()?, socket2::Type::DGRAM); + crate::arch::windows::setup_socket_for_win(socket2_socket, bind_addr, bind_dev, is_udp)?; + } + + if bind_addr.is_ipv6() { + socket2_socket.set_only_v6(true)?; + } + + socket2_socket.set_nonblocking(true)?; + socket2_socket.set_reuse_address(true)?; + socket2_socket.bind(&socket2::SockAddr::from(*bind_addr))?; + + // #[cfg(all(unix, not(target_os = "solaris"), not(target_os = "illumos")))] + // socket2_socket.set_reuse_port(true)?; + + if bind_addr.ip().is_unspecified() { + return Ok(()); + } + + // linux/mac does not use interface of bind_addr to send packet, so we need to bind device + // win can handle this with bind correctly + #[cfg(any(target_os = "ios", target_os = "macos"))] + if let Some(dev_name) = bind_dev { + // use IP_BOUND_IF to bind device + unsafe { + let dev_idx = nix::libc::if_nametoindex(dev_name.as_str().as_ptr() as *const i8); + tracing::warn!(?dev_idx, ?dev_name, "bind device"); + socket2_socket.bind_device_by_index_v4(std::num::NonZeroU32::new(dev_idx))?; + tracing::warn!(?dev_idx, ?dev_name, "bind device doen"); + } + } + + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + if let Some(dev_name) = bind_dev { + tracing::trace!(dev_name = ?dev_name, "bind device"); + socket2_socket.bind_device(Some(dev_name.as_bytes()))?; + } + + Ok(()) +} + +pub(crate) async fn wait_for_connect_futures( + mut futures: FuturesUnordered, +) -> Result +where + Fut: Future> + Send + Sync, + E: std::error::Error + Into + Send + Sync + 'static, +{ + // return last error + let mut last_err = None; + + while let Some(ret) = futures.next().await { + if let Err(e) = ret { + last_err = Some(e.into()); + } else { + return ret.map_err(|e| e.into()); + } + } + + Err(last_err.unwrap_or(TunnelError::Shutdown)) +} + +pub(crate) fn setup_sokcet2( + socket2_socket: &socket2::Socket, + bind_addr: &SocketAddr, +) -> Result<(), TunnelError> { + setup_sokcet2_ext( + socket2_socket, + bind_addr, + super::common::get_interface_name_by_ip(&bind_addr.ip()), + ) +} + +pub fn reserve_buf(buf: &mut BytesMut, min_size: usize, max_size: usize) { + if buf.capacity() < min_size { + buf.reserve(max_size); + } +} + +pub mod tests { + use std::time::Instant; + + use futures::{Future, SinkExt, StreamExt, TryStreamExt}; + use tokio_util::bytes::{BufMut, Bytes, BytesMut}; + + use crate::{ + common::netns::NetNS, + tunnel::{packet_def::ZCPacket, TunnelConnector, TunnelListener}, + }; + + pub async fn _tunnel_echo_server(tunnel: Box, once: bool) { + let (mut recv, mut send) = tunnel.split(); + + if !once { + while let Some(item) = recv.next().await { + let Ok(msg) = item else { + continue; + }; + if let Err(_) = send.send(msg).await { + break; + } + } + } else { + let Some(ret) = recv.next().await else { + assert!(false, "recv error"); + return; + }; + + if ret.is_err() { + tracing::debug!(?ret, "recv error"); + return; + } + + let res = ret.unwrap(); + tracing::debug!(?res, "recv a msg, try echo back"); + send.send(res).await.unwrap(); + } + let _ = send.flush().await; + let _ = send.close().await; + + tracing::warn!("echo server exit..."); + } + + pub(crate) async fn _tunnel_pingpong(listener: L, connector: C) + where + L: TunnelListener + Send + Sync + 'static, + C: TunnelConnector + Send + Sync + 'static, + { + _tunnel_pingpong_netns( + listener, + connector, + NetNS::new(None), + NetNS::new(None), + "12345678abcdefg".as_bytes().to_vec(), + ) + .await; + } + + pub(crate) async fn _tunnel_pingpong_netns( + mut listener: L, + mut connector: C, + l_netns: NetNS, + c_netns: NetNS, + buf: Vec, + ) where + L: TunnelListener + Send + Sync + 'static, + C: TunnelConnector + Send + Sync + 'static, + { + l_netns + .run_async(|| async { + listener.listen().await.unwrap(); + }) + .await; + + let lis = tokio::spawn(async move { + let ret = listener.accept().await.unwrap(); + println!("accept: {:?}", ret.info()); + assert_eq!( + url::Url::from(ret.info().unwrap().local_addr.unwrap()), + listener.local_url() + ); + _tunnel_echo_server(ret, false).await + }); + + let tunnel = c_netns.run_async(|| connector.connect()).await.unwrap(); + println!("connect: {:?}", tunnel.info()); + + assert_eq!( + url::Url::from(tunnel.info().unwrap().remote_addr.unwrap()), + connector.remote_url(), + ); + + let (mut recv, mut send) = tunnel.split(); + + send.send(ZCPacket::new_with_payload(buf.as_slice())) + .await + .unwrap(); + + let ret = tokio::time::timeout(tokio::time::Duration::from_secs(1), recv.next()) + .await + .unwrap() + .unwrap() + .unwrap(); + println!("echo back: {:?}", ret); + assert_eq!(ret.payload(), Bytes::from(buf)); + + send.close().await.unwrap(); + + if ["udp", "wg"].contains(&connector.remote_url().scheme()) { + lis.abort(); + } else { + // lis should finish in 1 second + let ret = tokio::time::timeout(tokio::time::Duration::from_secs(1), lis).await; + assert!(ret.is_ok()); + } + } + + pub(crate) async fn _tunnel_bench(mut listener: L, mut connector: C) + where + L: TunnelListener + Send + Sync + 'static, + C: TunnelConnector + Send + Sync + 'static, + { + listener.listen().await.unwrap(); + + let lis = tokio::spawn(async move { + let ret = listener.accept().await.unwrap(); + _tunnel_echo_server(ret, false).await + }); + + let tunnel = connector.connect().await.unwrap(); + + let (recv, mut send) = tunnel.split(); + + // prepare a 4k buffer with random data + let mut send_buf = BytesMut::new(); + for _ in 0..64 { + send_buf.put_i128(rand::random::()); + } + + let r = tokio::spawn(async move { + let now = Instant::now(); + let count = recv + .try_fold(0usize, |mut ret, _| async move { + ret += 1; + Ok(ret) + }) + .await + .unwrap(); + + println!( + "bps: {}", + (count / 1024) * 4 / now.elapsed().as_secs() as usize + ); + }); + + let now = Instant::now(); + while now.elapsed().as_secs() < 10 { + // send.feed(item) + let item = ZCPacket::new_with_payload(send_buf.as_ref()); + let _ = send.feed(item).await.unwrap(); + } + + send.close().await.unwrap(); + drop(send); + drop(connector); + drop(tunnel); + + tracing::warn!("wait for recv to finish..."); + + let _ = tokio::join!(r); + + lis.abort(); + let _ = tokio::join!(lis); + } + + pub fn enable_log() { + let filter = tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing::level_filters::LevelFilter::TRACE.into()) + .from_env() + .unwrap() + .add_directive("tarpc=error".parse().unwrap()); + tracing_subscriber::fmt::fmt() + .pretty() + .with_env_filter(filter) + .init(); + } + + pub async fn wait_for_condition(mut condition: F, timeout: std::time::Duration) -> () + where + F: FnMut() -> FRet + Send, + FRet: Future, + { + let now = std::time::Instant::now(); + while now.elapsed() < timeout { + if condition().await { + return; + } + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + assert!(condition().await, "Timeout") + } +} diff --git a/rust/easytier/src/tunnel/filter.rs b/rust/easytier/src/tunnel/filter.rs new file mode 100644 index 0000000..da588f2 --- /dev/null +++ b/rust/easytier/src/tunnel/filter.rs @@ -0,0 +1,363 @@ +use std::{ + sync::Arc, + task::{Context, Poll}, +}; + +use auto_impl::auto_impl; +use futures::{Sink, SinkExt, Stream, StreamExt}; + +use crate::proto::common::TunnelInfo; + +use self::stats::Throughput; + +use super::*; + +#[auto_impl(Arc, Box)] +pub trait TunnelFilter: Send + Sync { + type FilterOutput; + + fn before_send(&self, data: SinkItem) -> Option { + Some(data) + } + + fn after_received(&self, data: StreamItem) -> Option { + match data { + Ok(v) => Some(Ok(v)), + Err(e) => Some(Err(e)), + } + } + + fn filter_output(&self) -> Self::FilterOutput; +} + +pub struct TunnelFilterChain { + a: A, + b: B, +} + +impl TunnelFilter for TunnelFilterChain +where + A: TunnelFilter, + B: TunnelFilter, +{ + type FilterOutput = (OA, OB); + fn before_send(&self, data: SinkItem) -> Option { + let data = self.a.before_send(data)?; + self.b.before_send(data) + } + fn after_received(&self, data: StreamItem) -> Option { + let data = self.b.after_received(data)?; + self.a.after_received(data) + } + fn filter_output(&self) -> Self::FilterOutput { + (self.a.filter_output(), self.b.filter_output()) + } +} + +impl TunnelFilterChain { + pub fn new(a: A, b: B) -> Self { + Self { a, b } + } + + pub fn chain(self, c: T) -> TunnelFilterChain { + TunnelFilterChain::new(self, c) + } +} + +pub struct EmptyFilter; +impl TunnelFilter for EmptyFilter { + type FilterOutput = (); + fn filter_output(&self) {} +} + +pub trait ToTunnelChain { + fn to_chain(self) -> TunnelFilterChain + where + Self: Sized, + { + TunnelFilterChain::new(EmptyFilter, self) + } +} + +impl> ToTunnelChain for T {} + +pub struct TunnelWithFilter { + inner: T, + filter: Arc, +} + +impl TunnelWithFilter +where + T: Tunnel + Send + 'static, + F: TunnelFilter + Send + 'static, +{ + pub fn new(inner: T, filter: F) -> Self { + Self { + inner, + filter: Arc::new(filter), + } + } + + fn wrap_sink(&self, sink: S) -> impl ZCPacketSink { + struct SinkWrapper { + sink: S, + filter: Arc, + } + + impl Sink for SinkWrapper + where + F: TunnelFilter + 'static, + S: ZCPacketSink + 'static + Unpin, + { + type Error = SinkError; + + fn poll_ready( + self: std::pin::Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.get_mut().sink.poll_ready_unpin(cx) + } + + fn start_send( + self: std::pin::Pin<&mut Self>, + item: ZCPacket, + ) -> Result<(), Self::Error> { + let Some(item) = self.filter.before_send(item) else { + return Ok(()); + }; + self.get_mut().sink.start_send_unpin(item) + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.get_mut().sink.poll_flush_unpin(cx) + } + + fn poll_close( + self: std::pin::Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.get_mut().sink.poll_close_unpin(cx) + } + } + + SinkWrapper { + sink, + filter: self.filter.clone(), + } + } + + fn wrap_stream(&self, stream: S) -> impl ZCPacketStream { + struct StreamWrapper { + stream: S, + filter: Arc, + } + + impl Stream for StreamWrapper + where + F: TunnelFilter + 'static, + S: ZCPacketStream + 'static + Unpin, + { + type Item = StreamItem; + + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + let self_mut = self.get_mut(); + loop { + match self_mut.stream.poll_next_unpin(cx) { + Poll::Ready(Some(ret)) => { + let Some(ret) = self_mut.filter.after_received(ret) else { + continue; + }; + return Poll::Ready(Some(ret)); + } + Poll::Ready(None) => { + return Poll::Ready(None); + } + Poll::Pending => { + return Poll::Pending; + } + } + } + } + } + + StreamWrapper { + stream, + filter: self.filter.clone(), + } + } +} + +impl Tunnel for TunnelWithFilter +where + T: Tunnel + Send + 'static, + F: TunnelFilter + Send + 'static, +{ + fn info(&self) -> Option { + self.inner.info() + } + + fn split(&self) -> (Pin>, Pin>) { + let (stream, sink) = self.inner.split(); + ( + Box::pin(self.wrap_stream(stream)), + Box::pin(self.wrap_sink(sink)), + ) + } +} + +pub struct PacketRecorderTunnelFilter { + pub received: Arc>>, + pub sent: Arc>>, +} + +impl TunnelFilter for PacketRecorderTunnelFilter { + type FilterOutput = (Vec, Vec); + + fn before_send(&self, data: SinkItem) -> Option { + self.received.lock().unwrap().push(data.clone()); + Some(data) + } + + fn after_received(&self, data: StreamItem) -> Option { + match data { + Ok(v) => { + self.sent.lock().unwrap().push(v.clone().into()); + Some(Ok(v)) + } + Err(e) => Some(Err(e)), + } + } + + fn filter_output(&self) -> Self::FilterOutput { + ( + self.received.lock().unwrap().clone(), + self.sent.lock().unwrap().clone(), + ) + } +} + +impl PacketRecorderTunnelFilter { + pub fn new() -> Self { + Self { + received: Arc::new(std::sync::Mutex::new(Vec::new())), + sent: Arc::new(std::sync::Mutex::new(Vec::new())), + } + } +} + +pub struct StatsRecorderTunnelFilter { + throughput: Arc, +} + +impl TunnelFilter for StatsRecorderTunnelFilter { + type FilterOutput = Arc; + + fn before_send(&self, data: SinkItem) -> Option { + self.throughput.record_tx_bytes(data.buf_len() as u64); + Some(data) + } + + fn after_received(&self, data: StreamItem) -> Option { + match data { + Ok(v) => { + self.throughput.record_rx_bytes(v.buf_len() as u64); + Some(Ok(v)) + } + Err(e) => Some(Err(e)), + } + } + + fn filter_output(&self) -> Self::FilterOutput { + self.throughput.clone() + } +} + +impl StatsRecorderTunnelFilter { + pub fn new() -> Self { + Self { + throughput: Arc::new(Throughput::new()), + } + } + + pub fn get_throughput(&self) -> Arc { + self.throughput.clone() + } +} + +#[cfg(test)] +pub mod tests { + use std::sync::atomic::{AtomicU32, Ordering}; + + use filter::ring::create_ring_tunnel_pair; + + use super::*; + + pub struct DropSendTunnelFilter { + start: AtomicU32, + end: AtomicU32, + cur: AtomicU32, + } + + impl TunnelFilter for DropSendTunnelFilter { + type FilterOutput = (); + + fn before_send(&self, data: SinkItem) -> Option { + self.cur.fetch_add(1, Ordering::SeqCst); + if self.cur.load(Ordering::SeqCst) >= self.start.load(Ordering::SeqCst) + && self.cur.load(std::sync::atomic::Ordering::SeqCst) + < self.end.load(Ordering::SeqCst) + { + tracing::trace!("drop packet: {:?}", data); + return None; + } + Some(data) + } + + fn filter_output(&self) {} + } + + impl DropSendTunnelFilter { + pub fn new(start: u32, end: u32) -> Self { + Self { + start: AtomicU32::new(start), + end: AtomicU32::new(end), + cur: AtomicU32::new(0), + } + } + } + + #[tokio::test] + async fn test_nested_filter() { + let filter = Arc::new( + PacketRecorderTunnelFilter::new() + .to_chain() + .chain(PacketRecorderTunnelFilter::new()) + .chain(PacketRecorderTunnelFilter::new()) + .chain(PacketRecorderTunnelFilter::new()), + ); + let (s, _b) = create_ring_tunnel_pair(); + let tunnel = TunnelWithFilter::new(s, filter.clone()); + + let (_r, mut s) = tunnel.split(); + s.send(ZCPacket::new_with_payload("ab".as_bytes())) + .await + .unwrap(); + + let out = filter.filter_output(); + + let a = out.0 .0 .0 .1; + let b = out.0 .0 .1; + let c = out.0 .1; + let _d = out.1; + + assert_eq!(1, a.0.len()); + assert_eq!(1, b.0.len()); + assert_eq!(1, c.0.len()); + } +} diff --git a/rust/easytier/src/tunnel/insecure_tls.rs b/rust/easytier/src/tunnel/insecure_tls.rs new file mode 100644 index 0000000..f829ee2 --- /dev/null +++ b/rust/easytier/src/tunnel/insecure_tls.rs @@ -0,0 +1,86 @@ +use std::sync::Arc; + +use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime}; + +/// Dummy certificate verifier that treats any certificate as valid. +/// NOTE, such verification is vulnerable to MITM attacks, but convenient for testing. +#[derive(Debug)] +struct SkipServerVerification(Arc); + +impl SkipServerVerification { + fn new(provider: Arc) -> Arc { + Arc::new(Self(provider)) + } +} + +impl rustls::client::danger::ServerCertVerifier for SkipServerVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp: &[u8], + _now: UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0.signature_verification_algorithms.supported_schemes() + } +} + +pub fn init_crypto_provider() { + let _ = + rustls::crypto::CryptoProvider::install_default(rustls::crypto::ring::default_provider()); +} + +pub fn get_insecure_tls_client_config() -> rustls::ClientConfig { + init_crypto_provider(); + let provider = rustls::crypto::CryptoProvider::get_default().unwrap(); + let mut config = rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(SkipServerVerification::new(provider.clone())) + .with_no_client_auth(); + config.enable_sni = false; + config.enable_early_data = false; + config +} + +pub fn get_insecure_tls_cert<'a>() -> (Vec>, PrivateKeyDer<'a>) { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let cert_der = cert.serialize_der().unwrap(); + let priv_key = cert.serialize_private_key_der(); + let priv_key = rustls::pki_types::PrivatePkcs8KeyDer::from(priv_key); + let cert_chain = vec![cert_der.clone().into()]; + + (cert_chain, priv_key.into()) +} diff --git a/rust/easytier/src/tunnel/mod.rs b/rust/easytier/src/tunnel/mod.rs new file mode 100644 index 0000000..b3deb4e --- /dev/null +++ b/rust/easytier/src/tunnel/mod.rs @@ -0,0 +1,293 @@ +use std::collections::hash_map::DefaultHasher; +use std::hash::Hasher; +use std::{net::SocketAddr, pin::Pin, sync::Arc}; + +use async_trait::async_trait; +use futures::{Sink, Stream}; +use std::fmt::Debug; + +use tokio::time::error::Elapsed; + +use crate::proto::common::TunnelInfo; + +use self::packet_def::ZCPacket; + +pub mod buf; +pub mod common; +pub mod filter; +pub mod mpsc; +pub mod packet_def; +pub mod ring; +pub mod stats; +pub mod tcp; +pub mod udp; + +pub const PROTO_PORT_OFFSET: &[(&str, u16)] = + &[("tcp", 0), ("udp", 0), ("wg", 1), ("ws", 1), ("wss", 2)]; + +#[cfg(feature = "wireguard")] +pub mod wireguard; + +#[cfg(feature = "quic")] +pub mod quic; + +#[cfg(feature = "websocket")] +pub mod websocket; + +#[cfg(any(feature = "quic", feature = "websocket"))] +pub mod insecure_tls; + +#[derive(thiserror::Error, Debug)] +pub enum TunnelError { + #[error("io error")] + IOError(#[from] std::io::Error), + #[error("invalid packet. msg: {0}")] + InvalidPacket(String), + #[error("exceed max packet size. max: {0}, input: {1}")] + ExceedMaxPacketSize(usize, usize), + + #[error("invalid protocol: {0}")] + InvalidProtocol(String), + #[error("invalid addr: {0}")] + InvalidAddr(String), + + #[error("internal error {0}")] + InternalError(String), + + #[error("conn id not match, expect: {0}, actual: {1}")] + ConnIdNotMatch(u32, u32), + #[error("buffer full")] + BufferFull, + + #[error("timeout")] + Timeout(#[from] Elapsed), + + #[error("anyhow error: {0}")] + Anyhow(#[from] anyhow::Error), + + #[error("shutdown")] + Shutdown, + + #[error("no dns record found")] + NoDnsRecordFound(IpVersion), + + #[cfg(feature = "websocket")] + #[error("websocket error: {0}")] + WebSocketError(#[from] tokio_websockets::Error), + + #[error("tunnel error: {0}")] + TunError(String), +} + +pub type StreamT = packet_def::ZCPacket; +pub type StreamItem = Result; +pub type SinkItem = packet_def::ZCPacket; +pub type SinkError = TunnelError; + +pub trait ZCPacketStream: Stream + Send {} +impl ZCPacketStream for T where T: Stream + Send {} +pub trait ZCPacketSink: Sink + Send {} +impl ZCPacketSink for T where T: Sink + Send {} + +#[auto_impl::auto_impl(Box, Arc)] +pub trait Tunnel: Send { + fn split(&self) -> (Pin>, Pin>); + fn info(&self) -> Option; +} + +#[auto_impl::auto_impl(Arc)] +pub trait TunnelConnCounter: 'static + Send + Sync + Debug { + fn get(&self) -> Option; +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum IpVersion { + V4, + V6, + Both, +} + +#[async_trait] +#[auto_impl::auto_impl(Box)] +pub trait TunnelListener: Send { + async fn listen(&mut self) -> Result<(), TunnelError>; + async fn accept(&mut self) -> Result, TunnelError>; + fn local_url(&self) -> url::Url; + fn get_conn_counter(&self) -> Arc> { + #[derive(Debug)] + struct FakeTunnelConnCounter {} + impl TunnelConnCounter for FakeTunnelConnCounter { + fn get(&self) -> Option { + None + } + } + Arc::new(Box::new(FakeTunnelConnCounter {})) + } +} + +#[async_trait] +#[auto_impl::auto_impl(Box)] +pub trait TunnelConnector: Send { + async fn connect(&mut self) -> Result, TunnelError>; + fn remote_url(&self) -> url::Url; + fn set_bind_addrs(&mut self, _addrs: Vec) {} + fn set_ip_version(&mut self, _ip_version: IpVersion) {} +} + +pub fn build_url_from_socket_addr(addr: &String, scheme: &str) -> url::Url { + if let Ok(sock_addr) = addr.parse::() { + let mut ret_url = url::Url::parse(format!("{}://0.0.0.0", scheme).as_str()).unwrap(); + ret_url.set_ip_host(sock_addr.ip()).unwrap(); + ret_url.set_port(Some(sock_addr.port())).unwrap(); + ret_url + } else { + url::Url::parse(format!("{}://{}", scheme, addr).as_str()).unwrap() + } +} + +impl std::fmt::Debug for dyn Tunnel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Tunnel") + .field("info", &self.info()) + .finish() + } +} + +impl std::fmt::Debug for dyn TunnelConnector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TunnelConnector") + .field("remote_url", &self.remote_url()) + .finish() + } +} + +impl std::fmt::Debug for dyn TunnelListener { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TunnelListener") + .field("local_url", &self.local_url()) + .finish() + } +} + +pub(crate) trait FromUrl { + fn from_url(url: url::Url, ip_version: IpVersion) -> Result + where + Self: Sized; +} + +pub(crate) fn check_scheme_and_get_socket_addr_ext( + url: &url::Url, + scheme: &str, + ip_version: IpVersion, +) -> Result +where + T: FromUrl, +{ + if url.scheme() != scheme { + return Err(TunnelError::InvalidProtocol(url.scheme().to_string())); + } + + Ok(T::from_url(url.clone(), ip_version)?) +} + +pub(crate) fn check_scheme_and_get_socket_addr( + url: &url::Url, + scheme: &str, +) -> Result +where + T: FromUrl, +{ + if url.scheme() != scheme { + return Err(TunnelError::InvalidProtocol(url.scheme().to_string())); + } + + Ok(T::from_url(url.clone(), IpVersion::Both)?) +} + +fn default_port(scheme: &str) -> Option { + match scheme { + "tcp" => Some(11010), + "udp" => Some(11010), + "ws" => Some(11011), + "wss" => Some(11012), + "quic" => Some(11012), + "wg" => Some(11011), + _ => None, + } +} + +impl FromUrl for SocketAddr { + fn from_url(url: url::Url, ip_version: IpVersion) -> Result { + let addrs = url.socket_addrs(|| default_port(url.scheme()))?; + tracing::debug!(?addrs, ?ip_version, ?url, "convert url to socket addrs"); + let addrs = addrs + .into_iter() + .filter(|addr| match ip_version { + IpVersion::V4 => addr.is_ipv4(), + IpVersion::V6 => addr.is_ipv6(), + IpVersion::Both => true, + }) + .collect::>(); + + use rand::seq::SliceRandom; + // randomly select one address + addrs + .choose(&mut rand::thread_rng()) + .copied() + .ok_or(TunnelError::NoDnsRecordFound(ip_version)) + } +} + +impl FromUrl for uuid::Uuid { + fn from_url(url: url::Url, _ip_version: IpVersion) -> Result { + let o = url.host_str().unwrap(); + let o = uuid::Uuid::parse_str(o).map_err(|e| TunnelError::InvalidAddr(e.to_string()))?; + Ok(o) + } +} + +pub struct TunnelUrl { + inner: url::Url, +} + +impl From for TunnelUrl { + fn from(url: url::Url) -> Self { + TunnelUrl { inner: url } + } +} + +impl From for url::Url { + fn from(url: TunnelUrl) -> Self { + url.into_inner() + } +} + +impl TunnelUrl { + pub fn into_inner(self) -> url::Url { + self.inner + } + + pub fn bind_dev(&self) -> Option { + self.inner.path().strip_prefix("/").and_then(|s| { + if s.is_empty() { + None + } else { + Some(String::from_utf8(percent_encoding::percent_decode_str(&s).collect()).unwrap()) + } + }) + } +} + +pub fn generate_digest_from_str(str1: &str, str2: &str, digest: &mut [u8]) { + let mut hasher = DefaultHasher::new(); + hasher.write(str1.as_bytes()); + hasher.write(str2.as_bytes()); + + assert_eq!(digest.len() % 8, 0, "digest length must be multiple of 8"); + + let shard_count = digest.len() / 8; + for i in 0..shard_count { + digest[i * 8..(i + 1) * 8].copy_from_slice(&hasher.finish().to_be_bytes()); + hasher.write(&digest[..(i + 1) * 8]); + } +} diff --git a/rust/easytier/src/tunnel/mpsc.rs b/rust/easytier/src/tunnel/mpsc.rs new file mode 100644 index 0000000..bc39a1b --- /dev/null +++ b/rust/easytier/src/tunnel/mpsc.rs @@ -0,0 +1,254 @@ +// this mod wrap tunnel to a mpsc tunnel, based on crossbeam_channel + +use std::{pin::Pin, time::Duration}; + +use anyhow::Context; +use tokio::time::timeout; + +use crate::common::scoped_task::ScopedTask; + +use super::{packet_def::ZCPacket, Tunnel, TunnelError, ZCPacketSink, ZCPacketStream}; + +// use tokio::sync::mpsc::{channel, error::TrySendError, Receiver, Sender}; +use tachyonix::{channel, Receiver, Sender, TrySendError}; + +use futures::SinkExt; + +#[derive(Clone)] +pub struct MpscTunnelSender(Sender); + +impl MpscTunnelSender { + pub async fn send(&self, item: ZCPacket) -> Result<(), TunnelError> { + self.0.send(item).await.with_context(|| "send error")?; + Ok(()) + } + + pub fn try_send(&self, item: ZCPacket) -> Result<(), TunnelError> { + self.0.try_send(item).map_err(|e| match e { + TrySendError::Full(_) => TunnelError::BufferFull, + TrySendError::Closed(_) => TunnelError::Shutdown, + }) + } +} + +pub struct MpscTunnel { + tx: Option>, + + tunnel: T, + stream: Option>>, + + task: ScopedTask<()>, +} + +impl MpscTunnel { + pub fn new(tunnel: T, send_timeout: Option) -> Self { + let (tx, mut rx) = channel(32); + let (stream, mut sink) = tunnel.split(); + + let task = tokio::spawn(async move { + loop { + if let Err(e) = Self::forward_one_round(&mut rx, &mut sink, send_timeout).await { + tracing::error!(?e, "forward error"); + break; + } + } + rx.close(); + let close_ret = timeout(Duration::from_secs(5), sink.close()).await; + tracing::warn!(?close_ret, "mpsc close sink"); + }); + + Self { + tx: Some(tx), + tunnel, + stream: Some(stream), + task: task.into(), + } + } + + async fn forward_one_round( + rx: &mut Receiver, + sink: &mut Pin>, + send_timeout_ms: Option, + ) -> Result<(), TunnelError> { + let item = rx.recv().await.with_context(|| "recv error")?; + if let Some(timeout_ms) = send_timeout_ms { + Self::forward_one_round_with_timeout(rx, sink, item, timeout_ms).await + } else { + Self::forward_one_round_no_timeout(rx, sink, item).await + } + } + + async fn forward_one_round_no_timeout( + rx: &mut Receiver, + sink: &mut Pin>, + initial_item: ZCPacket, + ) -> Result<(), TunnelError> { + sink.feed(initial_item).await?; + + while let Ok(item) = rx.try_recv() { + match sink.feed(item).await { + Err(e) => { + tracing::error!(?e, "feed error"); + return Err(e); + } + Ok(_) => {} + } + } + + sink.flush().await + } + + async fn forward_one_round_with_timeout( + rx: &mut Receiver, + sink: &mut Pin>, + initial_item: ZCPacket, + timeout_ms: Duration, + ) -> Result<(), TunnelError> { + match timeout(timeout_ms, async move { + Self::forward_one_round_no_timeout(rx, sink, initial_item).await + }) + .await + { + Ok(Ok(_)) => Ok(()), + Ok(Err(e)) => { + tracing::error!(?e, "forward error"); + Err(e) + } + Err(e) => { + tracing::error!(?e, "forward timeout"); + Err(e.into()) + } + } + } + + pub fn get_stream(&mut self) -> Pin> { + self.stream.take().unwrap() + } + + pub fn get_sink(&self) -> MpscTunnelSender { + MpscTunnelSender(self.tx.as_ref().unwrap().clone()) + } + + pub fn close(&mut self) { + self.tx.take(); + self.task.abort(); + } +} + +#[cfg(test)] +mod tests { + use futures::StreamExt; + + use crate::tunnel::{ + ring::{create_ring_tunnel_pair, RING_TUNNEL_CAP}, + tcp::{TcpTunnelConnector, TcpTunnelListener}, + TunnelConnector, TunnelListener, + }; + + use super::*; + // test slow send lock in framed tunnel + #[tokio::test] + async fn mpsc_slow_receiver() { + let mut listener = TcpTunnelListener::new("tcp://127.0.0.1:11014".parse().unwrap()); + let mut connector = TcpTunnelConnector::new("tcp://127.0.0.1:11014".parse().unwrap()); + + listener.listen().await.unwrap(); + let t1 = tokio::spawn(async move { + let t = listener.accept().await.unwrap(); + let (mut stream, _sink) = t.split(); + let now = tokio::time::Instant::now(); + + let mut a_counter = 0; + let mut b_counter = 0; + + while let Some(Ok(msg)) = stream.next().await { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + if now.elapsed().as_secs() > 5 { + break; + } + + if msg.payload() == "hello".as_bytes() { + a_counter += 1; + } else if msg.payload() == "hello2".as_bytes() { + b_counter += 1; + } + } + + tracing::info!("t1 exit"); + assert_ne!(a_counter, 0); + assert_ne!(b_counter, 0); + }); + + let tunnel = connector.connect().await.unwrap(); + let mpsc_tunnel = MpscTunnel::new(tunnel, None); + + let sink1 = mpsc_tunnel.get_sink(); + let t2 = tokio::spawn(async move { + for i in 0..1000000 { + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + let a = sink1 + .send(ZCPacket::new_with_payload("hello".as_bytes())) + .await; + if a.is_err() { + tracing::info!(?a, "t2 exit with err"); + break; + } + + if i % 5000 == 0 { + tracing::info!(i, "send2 1000"); + } + } + + tracing::info!("t2 exit"); + }); + + let sink2 = mpsc_tunnel.get_sink(); + let t3 = tokio::spawn(async move { + for i in 0..1000000 { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + let a = sink2 + .send(ZCPacket::new_with_payload("hello2".as_bytes())) + .await; + if a.is_err() { + tracing::info!(?a, "t3 exit with err"); + break; + } + + if i % 5000 == 0 { + tracing::info!(i, "send2 1000"); + } + } + + tracing::info!("t3 exit"); + }); + + let t4 = tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + tracing::info!("closing"); + drop(mpsc_tunnel); + tracing::info!("closed"); + }); + + let _ = tokio::join!(t1, t2, t3, t4); + } + + #[tokio::test] + async fn mpsc_slow_receiver_with_send_timeout() { + let (a, _b) = create_ring_tunnel_pair(); + let mpsc_tunnel = MpscTunnel::new(a, Some(Duration::from_secs(1))); + let s = mpsc_tunnel.get_sink(); + for _ in 0..RING_TUNNEL_CAP { + s.send(ZCPacket::new_with_payload(&[0; 1024])) + .await + .unwrap(); + } + tokio::time::sleep(Duration::from_millis(1500)).await; + let e = s.send(ZCPacket::new_with_payload(&[0; 1024])).await; + assert!(e.is_ok()); + + tokio::time::sleep(Duration::from_millis(1500)).await; + + let e = s.send(ZCPacket::new_with_payload(&[0; 1024])).await; + assert!(e.is_err()); + } +} diff --git a/rust/easytier/src/tunnel/packet_def.rs b/rust/easytier/src/tunnel/packet_def.rs new file mode 100644 index 0000000..43956e8 --- /dev/null +++ b/rust/easytier/src/tunnel/packet_def.rs @@ -0,0 +1,685 @@ +use bytes::Bytes; +use bytes::BytesMut; +use zerocopy::byteorder::*; +use zerocopy::AsBytes; +use zerocopy::FromBytes; +use zerocopy::FromZeroes; + +type DefaultEndian = LittleEndian; + +const fn max(a: usize, b: usize) -> usize { + [a, b][(a < b) as usize] +} + +// TCP TunnelHeader +#[repr(C, packed)] +#[derive(AsBytes, FromBytes, FromZeroes, Clone, Debug, Default)] +pub struct TCPTunnelHeader { + pub len: U32, +} +pub const TCP_TUNNEL_HEADER_SIZE: usize = std::mem::size_of::(); + +#[derive(AsBytes, FromZeroes, Clone, Debug)] +#[repr(u8)] +pub enum UdpPacketType { + Invalid = 0, + Syn = 1, + Sack = 2, + Data = 3, + Fin = 4, + HolePunch = 5, +} + +#[repr(C, packed)] +#[derive(AsBytes, FromBytes, FromZeroes, Clone, Debug, Default)] +pub struct UDPTunnelHeader { + pub conn_id: U32, + pub msg_type: u8, + pub padding: u8, + pub len: U16, +} +pub const UDP_TUNNEL_HEADER_SIZE: usize = std::mem::size_of::(); + +#[repr(C, packed)] +#[derive(AsBytes, FromBytes, FromZeroes, Clone, Debug, Default)] +pub struct WGTunnelHeader { + pub ipv4_header: [u8; 20], +} +pub const WG_TUNNEL_HEADER_SIZE: usize = std::mem::size_of::(); + +#[derive(AsBytes, FromZeroes, Clone, Debug)] +#[repr(u8)] +pub enum PacketType { + Invalid = 0, + Data = 1, + HandShake = 2, + RoutePacket = 3, // deprecated + Ping = 4, + Pong = 5, + TaRpc = 6, // deprecated + Route = 7, // deprecated + RpcReq = 8, + RpcResp = 9, + ForeignNetworkPacket = 10, + KcpSrc = 11, + KcpDst = 12, +} + +bitflags::bitflags! { + struct PeerManagerHeaderFlags: u8 { + const ENCRYPTED = 0b0000_0001; + const LATENCY_FIRST = 0b0000_0010; + const EXIT_NODE = 0b0000_0100; + const NO_PROXY = 0b0000_1000; + const COMPRESSED = 0b0001_0000; + + const _ = !0; + } +} + +#[repr(C, packed)] +#[derive(AsBytes, FromBytes, FromZeroes, Clone, Debug, Default)] +pub struct PeerManagerHeader { + pub from_peer_id: U32, + pub to_peer_id: U32, + pub packet_type: u8, + pub flags: u8, + pub forward_counter: u8, + reserved: u8, + pub len: U32, +} +pub const PEER_MANAGER_HEADER_SIZE: usize = std::mem::size_of::(); + +impl PeerManagerHeader { + pub fn is_encrypted(&self) -> bool { + PeerManagerHeaderFlags::from_bits(self.flags) + .unwrap() + .contains(PeerManagerHeaderFlags::ENCRYPTED) + } + + pub fn set_encrypted(&mut self, encrypted: bool) { + let mut flags = PeerManagerHeaderFlags::from_bits(self.flags).unwrap(); + if encrypted { + flags.insert(PeerManagerHeaderFlags::ENCRYPTED); + } else { + flags.remove(PeerManagerHeaderFlags::ENCRYPTED); + } + self.flags = flags.bits(); + } + + pub fn is_latency_first(&self) -> bool { + PeerManagerHeaderFlags::from_bits(self.flags) + .unwrap() + .contains(PeerManagerHeaderFlags::LATENCY_FIRST) + } + + pub fn is_exit_node(&self) -> bool { + PeerManagerHeaderFlags::from_bits(self.flags) + .unwrap() + .contains(PeerManagerHeaderFlags::EXIT_NODE) + } + + pub fn is_no_proxy(&self) -> bool { + PeerManagerHeaderFlags::from_bits(self.flags) + .unwrap() + .contains(PeerManagerHeaderFlags::NO_PROXY) + } + + pub fn is_compressed(&self) -> bool { + PeerManagerHeaderFlags::from_bits(self.flags) + .unwrap() + .contains(PeerManagerHeaderFlags::COMPRESSED) + } + + pub fn set_latency_first(&mut self, latency_first: bool) -> &mut Self { + let mut flags = PeerManagerHeaderFlags::from_bits(self.flags).unwrap(); + if latency_first { + flags.insert(PeerManagerHeaderFlags::LATENCY_FIRST); + } else { + flags.remove(PeerManagerHeaderFlags::LATENCY_FIRST); + } + self.flags = flags.bits(); + self + } + + pub fn set_exit_node(&mut self, exit_node: bool) -> &mut Self { + let mut flags = PeerManagerHeaderFlags::from_bits(self.flags).unwrap(); + if exit_node { + flags.insert(PeerManagerHeaderFlags::EXIT_NODE); + } else { + flags.remove(PeerManagerHeaderFlags::EXIT_NODE); + } + self.flags = flags.bits(); + self + } + + pub fn set_no_proxy(&mut self, no_proxy: bool) -> &mut Self { + let mut flags = PeerManagerHeaderFlags::from_bits(self.flags).unwrap(); + if no_proxy { + flags.insert(PeerManagerHeaderFlags::NO_PROXY); + } else { + flags.remove(PeerManagerHeaderFlags::NO_PROXY); + } + self.flags = flags.bits(); + self + } + + pub fn set_compressed(&mut self, compressed: bool) -> &mut Self { + let mut flags = PeerManagerHeaderFlags::from_bits(self.flags).unwrap(); + if compressed { + flags.insert(PeerManagerHeaderFlags::COMPRESSED); + } else { + flags.remove(PeerManagerHeaderFlags::COMPRESSED); + } + self.flags = flags.bits(); + self + } +} + +#[repr(C, packed)] +#[derive(AsBytes, FromBytes, FromZeroes, Clone, Debug, Default)] +pub struct ForeignNetworkPacketHeader { + pub header_len: U16, + pub dst_peer_id: U32, + pub network_name_offset: U16, + pub network_name_len: U16, + /* variable length network_name string */ +} + +impl ForeignNetworkPacketHeader { + pub fn new(dst_peer_id: u32, network_name: &str) -> Self { + let network_name_offset = std::mem::size_of::() as u16; + let network_name_len = network_name.len() as u16; + let header_len = network_name_offset + network_name_len; + Self { + header_len: U16::new(header_len), + dst_peer_id: U32::new(dst_peer_id), + network_name_offset: U16::new(network_name_offset), + network_name_len: U16::new(network_name_len), + } + } + + pub fn get_network_name(&self, zc_packet_payload: &[u8]) -> String { + let offset = self.network_name_offset.get() as usize; + let len = self.network_name_len.get() as usize; + std::str::from_utf8(&zc_packet_payload[offset..offset + len]) + .unwrap() + .to_string() + } + + pub fn get_dst_peer_id(&self) -> u32 { + self.dst_peer_id.get() + } + + pub fn get_header_len(&self) -> usize { + self.header_len.get() as usize + } +} + +// reserve the space for aes tag and nonce +#[repr(C, packed)] +#[derive(AsBytes, FromBytes, FromZeroes, Clone, Debug, Default)] +pub struct AesGcmTail { + pub tag: [u8; 16], + pub nonce: [u8; 12], +} +pub const AES_GCM_ENCRYPTION_RESERVED: usize = std::mem::size_of::(); + +#[derive(AsBytes, FromZeroes, Clone, Debug, Copy)] +#[repr(u8)] +pub enum CompressorAlgo { + None = 0, + ZstdDefault = 1, +} + +#[repr(C, packed)] +#[derive(AsBytes, FromBytes, FromZeroes, Clone, Debug, Default)] +pub struct CompressorTail { + pub algo: u8, +} +pub const COMPRESSOR_TAIL_SIZE: usize = std::mem::size_of::(); + +impl CompressorTail { + pub fn get_algo(&self) -> Option { + match self.algo { + 1 => Some(CompressorAlgo::ZstdDefault), + _ => None, + } + } + + pub fn new(algo: CompressorAlgo) -> Self { + Self { algo: algo as u8 } + } +} + +pub const TAIL_RESERVED_SIZE: usize = max(AES_GCM_ENCRYPTION_RESERVED, COMPRESSOR_TAIL_SIZE); + +#[derive(Default, Debug)] +pub struct ZCPacketOffsets { + pub payload_offset: usize, + pub peer_manager_header_offset: usize, + pub tcp_tunnel_header_offset: usize, + pub udp_tunnel_header_offset: usize, + pub wg_tunnel_header_offset: usize, + pub dummy_tunnel_header_offset: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ZCPacketType { + // received from peer tcp connection + TCP, + // received from peer udp connection + UDP, + // received from peer wireguard connection + WG, + // received from local tun device, should reserve header space for tcp or udp tunnel + NIC, + // tunnel without header + DummyTunnel, +} + +const PAYLOAD_OFFSET_FOR_NIC_PACKET: usize = max( + max(TCP_TUNNEL_HEADER_SIZE, UDP_TUNNEL_HEADER_SIZE), + WG_TUNNEL_HEADER_SIZE, +) + PEER_MANAGER_HEADER_SIZE; + +// UDP Tunnel: TUN MTU + 24 (Easy) + 20 (Encrypted) + 8(UDP) + 20(IP) = TUN MTU + 72 +// TCP Tunnel: TUN MTU + 20 (Easy) + 20 (Encrypted) + 20(TCP) + 20(IP) = TUN MTU + 80 + +const INVALID_OFFSET: usize = usize::MAX; + +const fn get_converted_offset(old_hdr_size: usize, new_hdr_size: usize) -> usize { + if old_hdr_size < new_hdr_size { + INVALID_OFFSET + } else { + old_hdr_size - new_hdr_size + } +} + +impl ZCPacketType { + pub fn get_packet_offsets(&self) -> ZCPacketOffsets { + match self { + ZCPacketType::TCP => ZCPacketOffsets { + payload_offset: TCP_TUNNEL_HEADER_SIZE + PEER_MANAGER_HEADER_SIZE, + peer_manager_header_offset: TCP_TUNNEL_HEADER_SIZE, + tcp_tunnel_header_offset: 0, + udp_tunnel_header_offset: get_converted_offset( + TCP_TUNNEL_HEADER_SIZE, + UDP_TUNNEL_HEADER_SIZE, + ), + wg_tunnel_header_offset: get_converted_offset( + TCP_TUNNEL_HEADER_SIZE, + WG_TUNNEL_HEADER_SIZE, + ), + dummy_tunnel_header_offset: get_converted_offset(TCP_TUNNEL_HEADER_SIZE, 0), + }, + ZCPacketType::UDP => ZCPacketOffsets { + payload_offset: UDP_TUNNEL_HEADER_SIZE + PEER_MANAGER_HEADER_SIZE, + peer_manager_header_offset: UDP_TUNNEL_HEADER_SIZE, + tcp_tunnel_header_offset: get_converted_offset( + UDP_TUNNEL_HEADER_SIZE, + TCP_TUNNEL_HEADER_SIZE, + ), + udp_tunnel_header_offset: 0, + wg_tunnel_header_offset: get_converted_offset( + UDP_TUNNEL_HEADER_SIZE, + WG_TUNNEL_HEADER_SIZE, + ), + dummy_tunnel_header_offset: get_converted_offset(UDP_TUNNEL_HEADER_SIZE, 0), + }, + ZCPacketType::WG => ZCPacketOffsets { + payload_offset: WG_TUNNEL_HEADER_SIZE + PEER_MANAGER_HEADER_SIZE, + peer_manager_header_offset: WG_TUNNEL_HEADER_SIZE, + tcp_tunnel_header_offset: get_converted_offset( + WG_TUNNEL_HEADER_SIZE, + TCP_TUNNEL_HEADER_SIZE, + ), + udp_tunnel_header_offset: get_converted_offset( + WG_TUNNEL_HEADER_SIZE, + UDP_TUNNEL_HEADER_SIZE, + ), + wg_tunnel_header_offset: 0, + dummy_tunnel_header_offset: get_converted_offset(WG_TUNNEL_HEADER_SIZE, 0), + }, + ZCPacketType::NIC => ZCPacketOffsets { + payload_offset: PAYLOAD_OFFSET_FOR_NIC_PACKET, + peer_manager_header_offset: PAYLOAD_OFFSET_FOR_NIC_PACKET + - PEER_MANAGER_HEADER_SIZE, + tcp_tunnel_header_offset: PAYLOAD_OFFSET_FOR_NIC_PACKET + - PEER_MANAGER_HEADER_SIZE + - TCP_TUNNEL_HEADER_SIZE, + udp_tunnel_header_offset: PAYLOAD_OFFSET_FOR_NIC_PACKET + - PEER_MANAGER_HEADER_SIZE + - UDP_TUNNEL_HEADER_SIZE, + wg_tunnel_header_offset: PAYLOAD_OFFSET_FOR_NIC_PACKET + - PEER_MANAGER_HEADER_SIZE + - WG_TUNNEL_HEADER_SIZE, + dummy_tunnel_header_offset: PAYLOAD_OFFSET_FOR_NIC_PACKET + - PEER_MANAGER_HEADER_SIZE, + }, + ZCPacketType::DummyTunnel => ZCPacketOffsets { + payload_offset: PEER_MANAGER_HEADER_SIZE, + peer_manager_header_offset: 0, + tcp_tunnel_header_offset: get_converted_offset(0, TCP_TUNNEL_HEADER_SIZE), + udp_tunnel_header_offset: get_converted_offset(0, UDP_TUNNEL_HEADER_SIZE), + wg_tunnel_header_offset: get_converted_offset(0, WG_TUNNEL_HEADER_SIZE), + dummy_tunnel_header_offset: 0, + }, + } + } +} + +#[derive(Debug, Clone)] +pub struct ZCPacket { + inner: BytesMut, + packet_type: ZCPacketType, +} + +impl ZCPacket { + pub fn new_nic_packet() -> Self { + Self { + inner: BytesMut::new(), + packet_type: ZCPacketType::NIC, + } + } + + pub fn new_from_buf(buf: BytesMut, packet_type: ZCPacketType) -> Self { + Self { + inner: buf, + packet_type, + } + } + + pub fn new_with_payload(payload: &[u8]) -> Self { + let mut ret = Self::new_nic_packet(); + let payload_off = ret.packet_type.get_packet_offsets().payload_offset; + let total_len = payload_off + payload.len(); + ret.inner.reserve(total_len); + unsafe { ret.inner.set_len(total_len) }; + ret.mut_payload()[..payload.len()].copy_from_slice(&payload); + ret + } + + pub fn new_for_tun(cap: usize, packet_info_len: usize) -> Self { + let mut ret = Self::new_nic_packet(); + ret.inner.reserve(cap); + let total_len = ret.packet_type.get_packet_offsets().payload_offset - packet_info_len; + unsafe { ret.inner.set_len(total_len) }; + ret + } + + pub fn new_for_foreign_network( + network_name: &String, + dst_peer_id: u32, + foreign_zc_packet: &ZCPacket, + ) -> Self { + let foreign_network_hdr = ForeignNetworkPacketHeader::new(dst_peer_id, &network_name); + let total_payload_len = + foreign_network_hdr.get_header_len() + foreign_zc_packet.tunnel_payload().len(); + + let mut ret = Self::new_nic_packet(); + let payload_off = ret.packet_type.get_packet_offsets().payload_offset; + ret.inner.reserve(payload_off + total_payload_len); + unsafe { ret.inner.set_len(payload_off + total_payload_len) }; + + let fixed_hdr_len = std::mem::size_of::(); + ret.mut_payload()[..fixed_hdr_len].copy_from_slice(foreign_network_hdr.as_bytes()); + + let name_offset = foreign_network_hdr.network_name_offset.get() as usize; + let name_len = foreign_network_hdr.network_name_len.get() as usize; + ret.mut_payload()[name_offset..name_offset + name_len] + .copy_from_slice(network_name.as_bytes()); + + ret.mut_payload()[foreign_network_hdr.get_header_len()..] + .copy_from_slice(foreign_zc_packet.tunnel_payload()); + + let hdr = ret.mut_peer_manager_header().unwrap(); + hdr.from_peer_id = 0.into(); + hdr.to_peer_id = 0.into(); + hdr.packet_type = PacketType::ForeignNetworkPacket as u8; + hdr.len.set(total_payload_len as u32); + + ret + } + + pub fn packet_type(&self) -> ZCPacketType { + self.packet_type + } + + pub fn payload_offset(&self) -> usize { + self.packet_type.get_packet_offsets().payload_offset + } + + pub fn mut_payload(&mut self) -> &mut [u8] { + let offset = self.payload_offset(); + &mut self.inner[offset..] + } + + pub fn mut_peer_manager_header(&mut self) -> Option<&mut PeerManagerHeader> { + PeerManagerHeader::mut_from_prefix( + &mut self.inner[self + .packet_type + .get_packet_offsets() + .peer_manager_header_offset..], + ) + } + + pub fn mut_tcp_tunnel_header(&mut self) -> Option<&mut TCPTunnelHeader> { + TCPTunnelHeader::mut_from_prefix( + &mut self.inner[self + .packet_type + .get_packet_offsets() + .tcp_tunnel_header_offset..], + ) + } + + pub fn mut_udp_tunnel_header(&mut self) -> Option<&mut UDPTunnelHeader> { + UDPTunnelHeader::mut_from_prefix( + &mut self.inner[self + .packet_type + .get_packet_offsets() + .udp_tunnel_header_offset..], + ) + } + + pub fn mut_wg_tunnel_header(&mut self) -> Option<&mut WGTunnelHeader> { + WGTunnelHeader::mut_from_prefix( + &mut self.inner[self + .packet_type + .get_packet_offsets() + .wg_tunnel_header_offset..], + ) + } + + // ref versions + pub fn payload(&self) -> &[u8] { + &self.inner[self.payload_offset()..] + } + + pub fn payload_bytes(mut self) -> BytesMut { + self.inner.split_off(self.payload_offset()) + } + + pub fn peer_manager_header(&self) -> Option<&PeerManagerHeader> { + PeerManagerHeader::ref_from_prefix( + &self.inner[self + .packet_type + .get_packet_offsets() + .peer_manager_header_offset..], + ) + } + + pub fn tcp_tunnel_header(&self) -> Option<&TCPTunnelHeader> { + TCPTunnelHeader::ref_from_prefix( + &self.inner[self + .packet_type + .get_packet_offsets() + .tcp_tunnel_header_offset..], + ) + } + + pub fn udp_tunnel_header(&self) -> Option<&UDPTunnelHeader> { + UDPTunnelHeader::ref_from_prefix( + &self.inner[self + .packet_type + .get_packet_offsets() + .udp_tunnel_header_offset..], + ) + } + + pub fn udp_payload(&self) -> &[u8] { + &self.inner[self + .packet_type + .get_packet_offsets() + .udp_tunnel_header_offset + + UDP_TUNNEL_HEADER_SIZE..] + } + + pub fn payload_len(&self) -> usize { + self.inner.len() - self.payload_offset() + } + + pub fn buf_len(&self) -> usize { + self.inner.len() + } + + pub fn fill_peer_manager_hdr(&mut self, from_peer_id: u32, to_peer_id: u32, packet_type: u8) { + let payload_len = self.payload_len(); + let hdr = self.mut_peer_manager_header().unwrap(); + hdr.from_peer_id.set(from_peer_id); + hdr.to_peer_id.set(to_peer_id); + hdr.packet_type = packet_type; + hdr.flags = 0; + hdr.forward_counter = 1; + hdr.len.set(payload_len as u32); + } + + pub fn tunnel_payload(&self) -> &[u8] { + &self.inner[self + .packet_type + .get_packet_offsets() + .peer_manager_header_offset..] + } + + pub fn tunnel_payload_bytes(mut self) -> BytesMut { + self.inner.split_off( + self.packet_type + .get_packet_offsets() + .peer_manager_header_offset, + ) + } + + pub fn convert_type(mut self, target_packet_type: ZCPacketType) -> Self { + if target_packet_type == self.packet_type { + return self; + } + + let new_offset = match target_packet_type { + ZCPacketType::TCP => { + self.packet_type + .get_packet_offsets() + .tcp_tunnel_header_offset + } + ZCPacketType::UDP => { + self.packet_type + .get_packet_offsets() + .udp_tunnel_header_offset + } + ZCPacketType::WG => { + self.packet_type + .get_packet_offsets() + .wg_tunnel_header_offset + } + ZCPacketType::DummyTunnel => { + self.packet_type + .get_packet_offsets() + .dummy_tunnel_header_offset + } + ZCPacketType::NIC => unreachable!(), + }; + + tracing::trace!(?self.packet_type, ?target_packet_type, ?new_offset, "convert zc packet type"); + + if new_offset == INVALID_OFFSET { + // copy peer manager header and payload to new buffer + let tunnel_payload = self.tunnel_payload(); + let new_pm_offset = target_packet_type + .get_packet_offsets() + .peer_manager_header_offset; + let mut buf = BytesMut::with_capacity(new_pm_offset + tunnel_payload.len()); + unsafe { buf.set_len(new_pm_offset) }; + buf.extend_from_slice(tunnel_payload); + return Self::new_from_buf(buf, target_packet_type); + } + + return Self::new_from_buf(self.inner.split_off(new_offset), target_packet_type); + } + + pub fn into_bytes(self) -> Bytes { + self.inner.freeze() + } + + pub fn inner(self) -> BytesMut { + self.inner + } + + pub fn mut_inner(&mut self) -> &mut BytesMut { + &mut self.inner + } + + pub fn is_lossy(&self) -> bool { + self.peer_manager_header() + .and_then(|hdr| Some(hdr.packet_type == PacketType::Data as u8)) + .unwrap_or(false) + } + + pub fn foreign_network_hdr(&self) -> Option<&ForeignNetworkPacketHeader> { + if self.peer_manager_header().unwrap().packet_type == PacketType::ForeignNetworkPacket as u8 + { + ForeignNetworkPacketHeader::ref_from_prefix(self.payload()) + } else { + None + } + } + + pub fn foreign_network_packet(mut self) -> Self { + let hdr = self.foreign_network_hdr().unwrap(); + let foreign_hdr_len = hdr.get_header_len(); + + Self::new_from_buf( + self.inner + .split_off(foreign_hdr_len + self.payload_offset()), + ZCPacketType::DummyTunnel, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_zc_packet() { + let payload = b"hello world"; + let mut packet = ZCPacket::new_with_payload(payload); + let peer_manager_header = packet.mut_peer_manager_header().unwrap(); + peer_manager_header.packet_type = PacketType::Data as u8; + peer_manager_header.len.set(payload.len() as u32); + + let tcp_tunnel_header = packet.mut_tcp_tunnel_header().unwrap(); + tcp_tunnel_header.len.set(payload.len() as u32); + + // let udp_tunnel_header = packet.mut_udp_tunnel_header().unwrap(); + // udp_tunnel_header.conn_id = 1; + // udp_tunnel_header.msg_type = 2; + // udp_tunnel_header.len = payload.len() as u32; + + assert_eq!(packet.payload(), b"hello world"); + assert_eq!(packet.payload_len(), 11); + println!("{:?}", packet.inner); + + let tcp_packet = packet.convert_type(ZCPacketType::TCP).into_bytes(); + assert_eq!(&tcp_packet[..1], b"\x0b"); + println!("{:?}", tcp_packet); + } +} diff --git a/rust/easytier/src/tunnel/quic.rs b/rust/easytier/src/tunnel/quic.rs new file mode 100644 index 0000000..692335f --- /dev/null +++ b/rust/easytier/src/tunnel/quic.rs @@ -0,0 +1,262 @@ +//! This example demonstrates how to make a QUIC connection that ignores the server certificate. +//! +//! Checkout the `README.md` for guidance. + +use std::{error::Error, net::SocketAddr, sync::Arc}; + +use crate::tunnel::{ + check_scheme_and_get_socket_addr_ext, + common::{FramedReader, FramedWriter, TunnelWrapper}, + TunnelInfo, +}; +use anyhow::Context; +use quinn::{crypto::rustls::QuicClientConfig, ClientConfig, Connection, Endpoint, ServerConfig}; + +use super::{ + check_scheme_and_get_socket_addr, + insecure_tls::{get_insecure_tls_cert, get_insecure_tls_client_config}, + IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelListener, +}; + +fn configure_client() -> ClientConfig { + ClientConfig::new(Arc::new( + QuicClientConfig::try_from(get_insecure_tls_client_config()).unwrap(), + )) +} + +/// Constructs a QUIC endpoint configured to listen for incoming connections on a certain address +/// and port. +/// +/// ## Returns +/// +/// - a stream of incoming QUIC connections +/// - server certificate serialized into DER format +#[allow(unused)] +pub fn make_server_endpoint(bind_addr: SocketAddr) -> Result<(Endpoint, Vec), Box> { + let (server_config, server_cert) = configure_server()?; + let endpoint = Endpoint::server(server_config, bind_addr)?; + Ok((endpoint, server_cert)) +} + +/// Returns default server configuration along with its certificate. +fn configure_server() -> Result<(ServerConfig, Vec), Box> { + let (certs, key) = get_insecure_tls_cert(); + + let mut server_config = ServerConfig::with_single_cert(certs.clone(), key.into())?; + let transport_config = Arc::get_mut(&mut server_config.transport).unwrap(); + transport_config.max_concurrent_uni_streams(10_u8.into()); + transport_config.max_concurrent_bidi_streams(10_u8.into()); + + Ok((server_config, certs[0].to_vec())) +} + +#[allow(unused)] +pub const ALPN_QUIC_HTTP: &[&[u8]] = &[b"hq-29"]; + +/// Runs a QUIC server bound to given address. + +struct ConnWrapper { + conn: Connection, +} + +impl Drop for ConnWrapper { + fn drop(&mut self) { + self.conn.close(0u32.into(), b"done"); + } +} + +pub struct QUICTunnelListener { + addr: url::Url, + endpoint: Option, + server_cert: Option>, +} + +impl QUICTunnelListener { + pub fn new(addr: url::Url) -> Self { + QUICTunnelListener { + addr, + endpoint: None, + server_cert: None, + } + } +} + +#[async_trait::async_trait] +impl TunnelListener for QUICTunnelListener { + async fn listen(&mut self) -> Result<(), TunnelError> { + let addr = check_scheme_and_get_socket_addr::(&self.addr, "quic")?; + let (endpoint, server_cert) = make_server_endpoint(addr).unwrap(); + self.endpoint = Some(endpoint); + self.server_cert = Some(server_cert); + + self.addr + .set_port(Some(self.endpoint.as_ref().unwrap().local_addr()?.port())) + .unwrap(); + + Ok(()) + } + + async fn accept(&mut self) -> Result, super::TunnelError> { + // accept a single connection + let incoming_conn = self.endpoint.as_ref().unwrap().accept().await.unwrap(); + let conn = incoming_conn.await.unwrap(); + println!( + "[server] connection accepted: addr={}", + conn.remote_address() + ); + let remote_addr = conn.remote_address(); + let (w, r) = conn.accept_bi().await.with_context(|| "accept_bi failed")?; + + let arc_conn = Arc::new(ConnWrapper { conn }); + + let info = TunnelInfo { + tunnel_type: "quic".to_owned(), + local_addr: Some(self.local_url().into()), + remote_addr: Some( + super::build_url_from_socket_addr(&remote_addr.to_string(), "quic").into(), + ), + }; + + Ok(Box::new(TunnelWrapper::new( + FramedReader::new_with_associate_data(r, 2000, Some(Box::new(arc_conn.clone()))), + FramedWriter::new_with_associate_data(w, Some(Box::new(arc_conn))), + Some(info), + ))) + } + + fn local_url(&self) -> url::Url { + self.addr.clone() + } +} + +pub struct QUICTunnelConnector { + addr: url::Url, + endpoint: Option, + ip_version: IpVersion, +} + +impl QUICTunnelConnector { + pub fn new(addr: url::Url) -> Self { + QUICTunnelConnector { + addr, + endpoint: None, + ip_version: IpVersion::Both, + } + } +} + +#[async_trait::async_trait] +impl TunnelConnector for QUICTunnelConnector { + async fn connect(&mut self) -> Result, super::TunnelError> { + let addr = check_scheme_and_get_socket_addr_ext::( + &self.addr, + "quic", + self.ip_version, + )?; + let local_addr = if addr.is_ipv4() { + "0.0.0.0:0" + } else { + "[::]:0" + }; + + let mut endpoint = Endpoint::client(local_addr.parse().unwrap())?; + endpoint.set_default_client_config(configure_client()); + + // connect to server + let connection = endpoint.connect(addr, "localhost").unwrap().await.unwrap(); + println!("[client] connected: addr={}", connection.remote_address()); + + let local_addr = endpoint.local_addr().unwrap(); + + self.endpoint = Some(endpoint); + + let (w, r) = connection + .open_bi() + .await + .with_context(|| "open_bi failed")?; + + let info = TunnelInfo { + tunnel_type: "quic".to_owned(), + local_addr: Some( + super::build_url_from_socket_addr(&local_addr.to_string(), "quic").into(), + ), + remote_addr: Some(self.addr.clone().into()), + }; + + let arc_conn = Arc::new(ConnWrapper { conn: connection }); + Ok(Box::new(TunnelWrapper::new( + FramedReader::new_with_associate_data(r, 4500, Some(Box::new(arc_conn.clone()))), + FramedWriter::new_with_associate_data(w, Some(Box::new(arc_conn))), + Some(info), + ))) + } + + fn remote_url(&self) -> url::Url { + self.addr.clone() + } + + fn set_ip_version(&mut self, ip_version: IpVersion) { + self.ip_version = ip_version; + } +} + +#[cfg(test)] +mod tests { + use crate::tunnel::{ + common::tests::{_tunnel_bench, _tunnel_pingpong}, + IpVersion, + }; + + use super::*; + + #[tokio::test] + async fn quic_pingpong() { + let listener = QUICTunnelListener::new("quic://0.0.0.0:21011".parse().unwrap()); + let connector = QUICTunnelConnector::new("quic://127.0.0.1:21011".parse().unwrap()); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn quic_bench() { + let listener = QUICTunnelListener::new("quic://0.0.0.0:21012".parse().unwrap()); + let connector = QUICTunnelConnector::new("quic://127.0.0.1:21012".parse().unwrap()); + _tunnel_bench(listener, connector).await + } + + #[tokio::test] + async fn ipv6_pingpong() { + let listener = QUICTunnelListener::new("quic://[::1]:31015".parse().unwrap()); + let connector = QUICTunnelConnector::new("quic://[::1]:31015".parse().unwrap()); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn ipv6_domain_pingpong() { + let listener = QUICTunnelListener::new("quic://[::1]:31016".parse().unwrap()); + let mut connector = + QUICTunnelConnector::new("quic://test.easytier.top:31016".parse().unwrap()); + connector.set_ip_version(IpVersion::V6); + _tunnel_pingpong(listener, connector).await; + + let listener = QUICTunnelListener::new("quic://127.0.0.1:31016".parse().unwrap()); + let mut connector = + QUICTunnelConnector::new("quic://test.easytier.top:31016".parse().unwrap()); + connector.set_ip_version(IpVersion::V4); + _tunnel_pingpong(listener, connector).await; + } + + #[tokio::test] + async fn test_alloc_port() { + // v4 + let mut listener = QUICTunnelListener::new("quic://0.0.0.0:0".parse().unwrap()); + listener.listen().await.unwrap(); + let port = listener.local_url().port().unwrap(); + assert!(port > 0); + + // v6 + let mut listener = QUICTunnelListener::new("quic://[::]:0".parse().unwrap()); + listener.listen().await.unwrap(); + let port = listener.local_url().port().unwrap(); + assert!(port > 0); + } +} diff --git a/rust/easytier/src/tunnel/ring.rs b/rust/easytier/src/tunnel/ring.rs new file mode 100644 index 0000000..60b5607 --- /dev/null +++ b/rust/easytier/src/tunnel/ring.rs @@ -0,0 +1,377 @@ +use std::{ + collections::HashMap, + fmt::Debug, + sync::Arc, + task::{ready, Poll}, +}; + +use async_ringbuf::{traits::*, AsyncHeapCons, AsyncHeapProd, AsyncHeapRb}; +use crossbeam::atomic::AtomicCell; + +use async_trait::async_trait; +use futures::{Sink, SinkExt, Stream, StreamExt}; +use once_cell::sync::Lazy; + +use tokio::sync::{ + mpsc::{UnboundedReceiver, UnboundedSender}, + Mutex, +}; + +use uuid::Uuid; + +use crate::tunnel::{SinkError, SinkItem}; + +use super::{ + build_url_from_socket_addr, check_scheme_and_get_socket_addr, common::TunnelWrapper, + StreamItem, Tunnel, TunnelConnector, TunnelError, TunnelInfo, TunnelListener, +}; + +pub static RING_TUNNEL_CAP: usize = 128; +static RING_TUNNEL_RESERVERD_CAP: usize = 4; + +type RingLock = parking_lot::Mutex<()>; + +type RingItem = SinkItem; + +pub struct RingTunnel { + id: Uuid, + + ring_cons_impl: AtomicCell>>, + ring_prod_impl: AtomicCell>>, +} + +impl RingTunnel { + fn id(&self) -> &Uuid { + &self.id + } + + pub fn new(cap: usize) -> Self { + let id = Uuid::new_v4(); + let ring_impl = AsyncHeapRb::new(std::cmp::max(RING_TUNNEL_RESERVERD_CAP * 2, cap)); + let (ring_prod_impl, ring_cons_impl) = ring_impl.split(); + Self { + id: id.clone(), + ring_cons_impl: AtomicCell::new(Some(ring_cons_impl)), + ring_prod_impl: AtomicCell::new(Some(ring_prod_impl)), + } + } + + pub fn new_with_id(id: Uuid, cap: usize) -> Self { + let mut ret = Self::new(cap); + ret.id = id; + ret + } +} + +impl Debug for RingTunnel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RingTunnel").field("id", &self.id).finish() + } +} + +pub struct RingStream { + id: Uuid, + ring_cons_impl: AsyncHeapCons, +} + +impl RingStream { + pub fn new(tunnel: Arc) -> Self { + Self { + id: tunnel.id.clone(), + ring_cons_impl: tunnel.ring_cons_impl.take().unwrap(), + } + } +} + +impl Stream for RingStream { + type Item = StreamItem; + + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let ret = ready!(self.get_mut().ring_cons_impl.poll_next_unpin(cx)); + match ret { + Some(item) => Poll::Ready(Some(Ok(item))), + None => Poll::Ready(None), + } + } +} + +impl Debug for RingStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RingStream") + .field("id", &self.id) + .field("len", &self.ring_cons_impl.base().occupied_len()) + .field("cap", &self.ring_cons_impl.base().capacity()) + .finish() + } +} + +pub struct RingSink { + id: Uuid, + ring_prod_impl: AsyncHeapProd, +} + +impl RingSink { + pub fn new(tunnel: Arc) -> Self { + Self { + id: tunnel.id.clone(), + ring_prod_impl: tunnel.ring_prod_impl.take().unwrap(), + } + } + + pub fn try_send(&mut self, item: RingItem) -> Result<(), RingItem> { + let base = self.ring_prod_impl.base(); + if base.occupied_len() >= base.capacity().get() - RING_TUNNEL_RESERVERD_CAP { + return Err(item); + } + self.ring_prod_impl.try_push(item) + } + + pub fn force_send(&mut self, item: RingItem) -> Result<(), RingItem> { + self.ring_prod_impl.try_push(item) + } +} + +impl Sink for RingSink { + type Error = SinkError; + + fn poll_ready( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let ret = ready!(self.get_mut().ring_prod_impl.poll_ready_unpin(cx)); + Poll::Ready(ret.map_err(|_| TunnelError::Shutdown)) + } + + fn start_send(self: std::pin::Pin<&mut Self>, item: SinkItem) -> Result<(), Self::Error> { + self.get_mut() + .ring_prod_impl + .start_send_unpin(item) + .map_err(|_| TunnelError::Shutdown) + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let ret = ready!(self.get_mut().ring_prod_impl.poll_flush_unpin(cx)); + Poll::Ready(ret.map_err(|_| TunnelError::Shutdown)) + } + + fn poll_close( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let ret = ready!(self.get_mut().ring_prod_impl.poll_close_unpin(cx)); + Poll::Ready(ret.map_err(|_| TunnelError::Shutdown)) + } +} + +impl Debug for RingSink { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RingSink") + .field("id", &self.id) + .field("len", &self.ring_prod_impl.base().occupied_len()) + .field("cap", &self.ring_prod_impl.base().capacity()) + .finish() + } +} + +struct Connection { + client: Arc, + server: Arc, +} + +static CONNECTION_MAP: Lazy>>>>> = + Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); + +#[derive(Debug)] +pub struct RingTunnelListener { + listerner_addr: url::Url, + conn_sender: UnboundedSender>, + conn_receiver: UnboundedReceiver>, +} + +impl RingTunnelListener { + pub fn new(key: url::Url) -> Self { + let (conn_sender, conn_receiver) = tokio::sync::mpsc::unbounded_channel(); + RingTunnelListener { + listerner_addr: key, + conn_sender, + conn_receiver, + } + } +} + +fn get_tunnel_for_client(conn: Arc) -> impl Tunnel { + TunnelWrapper::new( + RingStream::new(conn.client.clone()), + RingSink::new(conn.server.clone()), + Some(TunnelInfo { + tunnel_type: "ring".to_owned(), + local_addr: Some(build_url_from_socket_addr(&conn.client.id.into(), "ring").into()), + remote_addr: Some(build_url_from_socket_addr(&conn.server.id.into(), "ring").into()), + }), + ) +} + +fn get_tunnel_for_server(conn: Arc) -> impl Tunnel { + TunnelWrapper::new( + RingStream::new(conn.server.clone()), + RingSink::new(conn.client.clone()), + Some(TunnelInfo { + tunnel_type: "ring".to_owned(), + local_addr: Some(build_url_from_socket_addr(&conn.server.id.into(), "ring").into()), + remote_addr: Some(build_url_from_socket_addr(&conn.client.id.into(), "ring").into()), + }), + ) +} + +impl RingTunnelListener { + fn get_addr(&self) -> Result { + check_scheme_and_get_socket_addr::(&self.listerner_addr, "ring") + } +} + +#[async_trait] +impl TunnelListener for RingTunnelListener { + async fn listen(&mut self) -> Result<(), TunnelError> { + tracing::info!("listen new conn of key: {}", self.listerner_addr); + CONNECTION_MAP + .lock() + .await + .insert(self.get_addr()?, self.conn_sender.clone()); + Ok(()) + } + + async fn accept(&mut self) -> Result, TunnelError> { + tracing::info!("waiting accept new conn of key: {}", self.listerner_addr); + let my_addr = self.get_addr()?; + if let Some(conn) = self.conn_receiver.recv().await { + if conn.server.id == my_addr { + tracing::info!("accept new conn of key: {}", self.listerner_addr); + return Ok(Box::new(get_tunnel_for_server(conn))); + } else { + tracing::error!(?conn.server.id, ?my_addr, "got new conn with wrong id"); + return Err(TunnelError::InternalError( + "accept got wrong ring server id".to_owned(), + )); + } + } + + return Err(TunnelError::InternalError( + "conn receiver stopped".to_owned(), + )); + } + + fn local_url(&self) -> url::Url { + self.listerner_addr.clone() + } +} + +pub struct RingTunnelConnector { + remote_addr: url::Url, +} + +impl RingTunnelConnector { + pub fn new(remote_addr: url::Url) -> Self { + RingTunnelConnector { remote_addr } + } +} + +#[async_trait] +impl TunnelConnector for RingTunnelConnector { + async fn connect(&mut self) -> Result, super::TunnelError> { + let remote_addr = check_scheme_and_get_socket_addr::(&self.remote_addr, "ring")?; + let entry = CONNECTION_MAP + .lock() + .await + .get(&remote_addr) + .unwrap() + .clone(); + tracing::info!("connecting"); + let conn = Arc::new(Connection { + client: Arc::new(RingTunnel::new(RING_TUNNEL_CAP)), + server: Arc::new(RingTunnel::new_with_id( + remote_addr.clone(), + RING_TUNNEL_CAP, + )), + }); + entry + .send(conn.clone()) + .map_err(|_| TunnelError::InternalError("send conn to listner failed".to_owned()))?; + Ok(Box::new(get_tunnel_for_client(conn))) + } + + fn remote_url(&self) -> url::Url { + self.remote_addr.clone() + } +} + +pub fn create_ring_tunnel_pair() -> (Box, Box) { + let conn = Arc::new(Connection { + client: Arc::new(RingTunnel::new(RING_TUNNEL_CAP)), + server: Arc::new(RingTunnel::new(RING_TUNNEL_CAP)), + }); + ( + Box::new(get_tunnel_for_server(conn.clone())), + Box::new(get_tunnel_for_client(conn)), + ) +} + +#[cfg(test)] +mod tests { + use futures::StreamExt; + use tokio::time::timeout; + + use crate::tunnel::common::tests::{_tunnel_bench, _tunnel_pingpong}; + + use super::*; + + #[tokio::test] + async fn ring_pingpong() { + let id: url::Url = format!("ring://{}", Uuid::new_v4()).parse().unwrap(); + let listener = RingTunnelListener::new(id.clone()); + let connector = RingTunnelConnector::new(id.clone()); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn ring_bench() { + let id: url::Url = format!("ring://{}", Uuid::new_v4()).parse().unwrap(); + let listener = RingTunnelListener::new(id.clone()); + let connector = RingTunnelConnector::new(id); + _tunnel_bench(listener, connector).await + } + + #[tokio::test] + async fn ring_close() { + let (stunnel, ctunnel) = create_ring_tunnel_pair(); + drop(stunnel); + + let mut stream = ctunnel.split().0; + let ret = stream.next().await; + assert!(ret.as_ref().is_none(), "expect none, got {:?}", ret); + } + + #[tokio::test] + async fn abort_ring_stream() { + let (_stunnel, ctunnel) = create_ring_tunnel_pair(); + let mut stream = ctunnel.split().0; + let task = tokio::spawn(async move { + let _ = stream.next().await; + }); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + task.abort(); + let _ = tokio::join!(task); + } + + #[tokio::test] + async fn ring_stream_recv_timeout() { + let (_stunnel, ctunnel) = create_ring_tunnel_pair(); + let mut stream = ctunnel.split().0; + let _ = timeout(tokio::time::Duration::from_millis(10), stream.next()).await; + } +} diff --git a/rust/easytier/src/tunnel/stats.rs b/rust/easytier/src/tunnel/stats.rs new file mode 100644 index 0000000..1446def --- /dev/null +++ b/rust/easytier/src/tunnel/stats.rs @@ -0,0 +1,106 @@ +use std::sync::atomic::{AtomicU32, Ordering::Relaxed}; + +pub struct WindowLatency { + latency_us_window: Vec, + latency_us_window_index: AtomicU32, + latency_us_window_size: u32, + + sum: AtomicU32, + count: AtomicU32, +} + +impl std::fmt::Debug for WindowLatency { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WindowLatency") + .field("count", &self.count) + .field("window_size", &self.latency_us_window_size) + .field("window_latency", &self.get_latency_us::()) + .finish() + } +} + +impl WindowLatency { + pub fn new(window_size: u32) -> Self { + Self { + latency_us_window: (0..window_size).map(|_| AtomicU32::new(0)).collect(), + latency_us_window_index: AtomicU32::new(0), + latency_us_window_size: window_size, + + sum: AtomicU32::new(0), + count: AtomicU32::new(0), + } + } + + pub fn record_latency(&self, latency_us: u32) { + let index = self.latency_us_window_index.fetch_add(1, Relaxed); + if self.count.load(Relaxed) < self.latency_us_window_size { + self.count.fetch_add(1, Relaxed); + } + + let index = index % self.latency_us_window_size; + let old_lat = self.latency_us_window[index as usize].swap(latency_us, Relaxed); + + if old_lat < latency_us { + self.sum.fetch_add(latency_us - old_lat, Relaxed); + } else { + self.sum.fetch_sub(old_lat - latency_us, Relaxed); + } + } + + pub fn get_latency_us + std::ops::Div>(&self) -> T { + let count = self.count.load(Relaxed); + let sum = self.sum.load(Relaxed); + if count == 0 { + 0.into() + } else { + (T::from(sum)) / T::from(count) + } + } +} + +#[derive(Default, Copy, Clone, Debug)] +pub struct Throughput { + tx_bytes: u64, + rx_bytes: u64, + + tx_packets: u64, + rx_packets: u64, +} + +impl Throughput { + pub fn new() -> Self { + Self::default() + } + + pub fn tx_bytes(&self) -> u64 { + self.tx_bytes + } + + pub fn rx_bytes(&self) -> u64 { + self.rx_bytes + } + + pub fn tx_packets(&self) -> u64 { + self.tx_packets + } + + pub fn rx_packets(&self) -> u64 { + self.rx_packets + } + + pub fn record_tx_bytes(&self, bytes: u64) { + #[allow(invalid_reference_casting)] + unsafe { + *(&self.tx_bytes as *const u64 as *mut u64) += bytes; + *(&self.tx_packets as *const u64 as *mut u64) += 1; + } + } + + pub fn record_rx_bytes(&self, bytes: u64) { + #[allow(invalid_reference_casting)] + unsafe { + *(&self.rx_bytes as *const u64 as *mut u64) += bytes; + *(&self.rx_packets as *const u64 as *mut u64) += 1; + } + } +} diff --git a/rust/easytier/src/tunnel/tcp.rs b/rust/easytier/src/tunnel/tcp.rs new file mode 100644 index 0000000..d5efaf7 --- /dev/null +++ b/rust/easytier/src/tunnel/tcp.rs @@ -0,0 +1,297 @@ +use std::net::SocketAddr; + +use async_trait::async_trait; +use futures::stream::FuturesUnordered; +use tokio::net::{TcpListener, TcpSocket, TcpStream}; + +use super::TunnelInfo; +use crate::tunnel::common::setup_sokcet2; + +use super::{ + check_scheme_and_get_socket_addr, check_scheme_and_get_socket_addr_ext, + common::{wait_for_connect_futures, FramedReader, FramedWriter, TunnelWrapper}, + IpVersion, Tunnel, TunnelError, TunnelListener, +}; + +const TCP_MTU_BYTES: usize = 2000; + +#[derive(Debug)] +pub struct TcpTunnelListener { + addr: url::Url, + listener: Option, +} + +impl TcpTunnelListener { + pub fn new(addr: url::Url) -> Self { + TcpTunnelListener { + addr, + listener: None, + } + } + + async fn do_accept(&mut self) -> Result, std::io::Error> { + let listener = self.listener.as_ref().unwrap(); + let (stream, _) = listener.accept().await?; + + if let Err(e) = stream.set_nodelay(true) { + tracing::warn!(?e, "set_nodelay fail in accept"); + } + + let info = TunnelInfo { + tunnel_type: "tcp".to_owned(), + local_addr: Some(self.local_url().into()), + remote_addr: Some( + super::build_url_from_socket_addr(&stream.peer_addr()?.to_string(), "tcp").into(), + ), + }; + + let (r, w) = stream.into_split(); + Ok(Box::new(TunnelWrapper::new( + FramedReader::new(r, TCP_MTU_BYTES), + FramedWriter::new(w), + Some(info), + ))) + } +} + +#[async_trait] +impl TunnelListener for TcpTunnelListener { + async fn listen(&mut self) -> Result<(), TunnelError> { + self.listener = None; + let addr = check_scheme_and_get_socket_addr::(&self.addr, "tcp")?; + + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(addr), + socket2::Type::STREAM, + Some(socket2::Protocol::TCP), + )?; + setup_sokcet2(&socket2_socket, &addr)?; + let socket = TcpSocket::from_std_stream(socket2_socket.into()); + + if let Err(e) = socket.set_nodelay(true) { + tracing::warn!(?e, "set_nodelay fail in listen"); + } + + self.addr + .set_port(Some(socket.local_addr()?.port())) + .unwrap(); + + self.listener = Some(socket.listen(1024)?); + Ok(()) + } + + async fn accept(&mut self) -> Result, super::TunnelError> { + loop { + match self.do_accept().await { + Ok(ret) => return Ok(ret), + Err(e) => { + use std::io::ErrorKind::*; + if matches!( + e.kind(), + NotConnected | ConnectionAborted | ConnectionRefused | ConnectionReset + ) { + tracing::warn!(?e, "accept fail with retryable error: {:?}", e); + continue; + } + tracing::warn!(?e, "accept fail"); + return Err(e.into()); + } + } + } + } + + fn local_url(&self) -> url::Url { + self.addr.clone() + } +} + +fn get_tunnel_with_tcp_stream( + stream: TcpStream, + remote_url: url::Url, +) -> Result, super::TunnelError> { + if let Err(e) = stream.set_nodelay(true) { + tracing::warn!(?e, "set_nodelay fail in get_tunnel_with_tcp_stream"); + } + + let info = TunnelInfo { + tunnel_type: "tcp".to_owned(), + local_addr: Some( + super::build_url_from_socket_addr(&stream.local_addr()?.to_string(), "tcp").into(), + ), + remote_addr: Some(remote_url.into()), + }; + + let (r, w) = stream.into_split(); + Ok(Box::new(TunnelWrapper::new( + FramedReader::new(r, TCP_MTU_BYTES), + FramedWriter::new(w), + Some(info), + ))) +} + +#[derive(Debug)] +pub struct TcpTunnelConnector { + addr: url::Url, + + bind_addrs: Vec, + ip_version: IpVersion, +} + +impl TcpTunnelConnector { + pub fn new(addr: url::Url) -> Self { + TcpTunnelConnector { + addr, + bind_addrs: vec![], + ip_version: IpVersion::Both, + } + } + + async fn connect_with_default_bind( + &mut self, + addr: SocketAddr, + ) -> Result, super::TunnelError> { + tracing::info!(addr = ?self.addr, "connect tcp start"); + let stream = TcpStream::connect(addr).await?; + tracing::info!(addr = ?self.addr, "connect tcp succ"); + return get_tunnel_with_tcp_stream(stream, self.addr.clone().into()); + } + + async fn connect_with_custom_bind( + &mut self, + addr: SocketAddr, + ) -> Result, super::TunnelError> { + let futures = FuturesUnordered::new(); + + for bind_addr in self.bind_addrs.iter() { + tracing::info!(bind_addr = ?bind_addr, ?addr, "bind addr"); + + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(addr), + socket2::Type::STREAM, + Some(socket2::Protocol::TCP), + )?; + + if let Err(e) = setup_sokcet2(&socket2_socket, bind_addr) { + tracing::error!(bind_addr = ?bind_addr, ?addr, "bind addr fail: {:?}", e); + continue; + } + + let socket = TcpSocket::from_std_stream(socket2_socket.into()); + futures.push(socket.connect(addr.clone())); + } + + let ret = wait_for_connect_futures(futures).await; + return get_tunnel_with_tcp_stream(ret?, self.addr.clone().into()); + } +} + +#[async_trait] +impl super::TunnelConnector for TcpTunnelConnector { + async fn connect(&mut self) -> Result, super::TunnelError> { + let addr = + check_scheme_and_get_socket_addr_ext::(&self.addr, "tcp", self.ip_version)?; + if self.bind_addrs.is_empty() || addr.is_ipv6() { + self.connect_with_default_bind(addr).await + } else { + self.connect_with_custom_bind(addr).await + } + } + + fn remote_url(&self) -> url::Url { + self.addr.clone() + } + + fn set_bind_addrs(&mut self, addrs: Vec) { + self.bind_addrs = addrs; + } + + fn set_ip_version(&mut self, ip_version: IpVersion) { + self.ip_version = ip_version; + } +} + +#[cfg(test)] +mod tests { + use crate::tunnel::{ + common::tests::{_tunnel_bench, _tunnel_pingpong}, + TunnelConnector, + }; + + use super::*; + + #[tokio::test] + async fn tcp_pingpong() { + let listener = TcpTunnelListener::new("tcp://0.0.0.0:31011".parse().unwrap()); + let connector = TcpTunnelConnector::new("tcp://127.0.0.1:31011".parse().unwrap()); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn tcp_bench() { + let listener = TcpTunnelListener::new("tcp://0.0.0.0:31012".parse().unwrap()); + let connector = TcpTunnelConnector::new("tcp://127.0.0.1:31012".parse().unwrap()); + _tunnel_bench(listener, connector).await + } + + #[tokio::test] + async fn tcp_bench_with_bind() { + let listener = TcpTunnelListener::new("tcp://127.0.0.1:11013".parse().unwrap()); + let mut connector = TcpTunnelConnector::new("tcp://127.0.0.1:11013".parse().unwrap()); + connector.set_bind_addrs(vec!["127.0.0.1:0".parse().unwrap()]); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + #[should_panic] + async fn tcp_bench_with_bind_fail() { + let listener = TcpTunnelListener::new("tcp://127.0.0.1:11014".parse().unwrap()); + let mut connector = TcpTunnelConnector::new("tcp://127.0.0.1:11014".parse().unwrap()); + connector.set_bind_addrs(vec!["10.0.0.1:0".parse().unwrap()]); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn bind_same_port() { + let mut listener = TcpTunnelListener::new("tcp://[::]:31014".parse().unwrap()); + let mut listener2 = TcpTunnelListener::new("tcp://0.0.0.0:31014".parse().unwrap()); + listener.listen().await.unwrap(); + listener2.listen().await.unwrap(); + } + + #[tokio::test] + async fn ipv6_pingpong() { + let listener = TcpTunnelListener::new("tcp://[::1]:31015".parse().unwrap()); + let connector = TcpTunnelConnector::new("tcp://[::1]:31015".parse().unwrap()); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn ipv6_domain_pingpong() { + let listener = TcpTunnelListener::new("tcp://[::1]:31015".parse().unwrap()); + let mut connector = + TcpTunnelConnector::new("tcp://test.easytier.top:31015".parse().unwrap()); + connector.set_ip_version(IpVersion::V6); + _tunnel_pingpong(listener, connector).await; + + let listener = TcpTunnelListener::new("tcp://127.0.0.1:31015".parse().unwrap()); + let mut connector = + TcpTunnelConnector::new("tcp://test.easytier.top:31015".parse().unwrap()); + connector.set_ip_version(IpVersion::V4); + _tunnel_pingpong(listener, connector).await; + } + + #[tokio::test] + async fn test_alloc_port() { + // v4 + let mut listener = TcpTunnelListener::new("tcp://0.0.0.0:0".parse().unwrap()); + listener.listen().await.unwrap(); + let port = listener.local_url().port().unwrap(); + assert!(port > 0); + + // v6 + let mut listener = TcpTunnelListener::new("tcp://[::]:0".parse().unwrap()); + listener.listen().await.unwrap(); + let port = listener.local_url().port().unwrap(); + assert!(port > 0); + } +} diff --git a/rust/easytier/src/tunnel/udp.rs b/rust/easytier/src/tunnel/udp.rs new file mode 100644 index 0000000..07c5d46 --- /dev/null +++ b/rust/easytier/src/tunnel/udp.rs @@ -0,0 +1,1035 @@ +use std::{ + fmt::Debug, + sync::{Arc, Weak}, +}; + +use anyhow::Context; +use async_trait::async_trait; +use bytes::BytesMut; +use dashmap::DashMap; +use futures::{stream::FuturesUnordered, StreamExt}; +use rand::{Rng, SeedableRng}; +use zerocopy::AsBytes; + +use std::net::SocketAddr; +use tokio::{ + net::UdpSocket, + sync::mpsc::{Receiver, Sender, UnboundedReceiver, UnboundedSender}, + task::JoinSet, +}; + +use tracing::{instrument, Instrument}; + +use super::TunnelInfo; +use crate::{ + common::{join_joinset_background, scoped_task::ScopedTask}, + tunnel::{ + build_url_from_socket_addr, + common::{reserve_buf, TunnelWrapper}, + packet_def::{UdpPacketType, ZCPacket, ZCPacketType}, + ring::RingTunnel, + }, +}; + +use super::{ + common::{setup_sokcet2, setup_sokcet2_ext, wait_for_connect_futures}, + packet_def::{UDPTunnelHeader, UDP_TUNNEL_HEADER_SIZE}, + ring::{RingSink, RingStream}, + IpVersion, Tunnel, TunnelConnCounter, TunnelError, TunnelListener, TunnelUrl, +}; + +pub const UDP_DATA_MTU: usize = 2000; + +type UdpCloseEventSender = UnboundedSender<(SocketAddr, Option)>; +type UdpCloseEventReceiver = UnboundedReceiver<(SocketAddr, Option)>; + +fn new_udp_packet(f: F, udp_body: Option<&mut [u8]>) -> ZCPacket +where + F: FnOnce(&mut UDPTunnelHeader), +{ + let mut buf = BytesMut::new(); + buf.resize( + UDP_TUNNEL_HEADER_SIZE + udp_body.as_ref().map(|v| v.len()).unwrap_or(0), + 0, + ); + buf[UDP_TUNNEL_HEADER_SIZE..].copy_from_slice(udp_body.unwrap()); + + let mut ret = ZCPacket::new_from_buf(buf, ZCPacketType::UDP); + let header = ret.mut_udp_tunnel_header().unwrap(); + f(header); + ret +} + +fn new_syn_packet(conn_id: u32, magic: u64) -> ZCPacket { + new_udp_packet( + |header| { + header.msg_type = UdpPacketType::Syn as u8; + header.conn_id.set(conn_id); + header.len.set(8); + }, + Some(&mut magic.to_le_bytes()), + ) +} + +fn new_sack_packet(conn_id: u32, magic: u64) -> ZCPacket { + new_udp_packet( + |header| { + header.msg_type = UdpPacketType::Sack as u8; + header.conn_id.set(conn_id); + header.len.set(8); + }, + Some(&mut magic.to_le_bytes()), + ) +} + +pub fn new_hole_punch_packet(tid: u32, buf_len: u16) -> ZCPacket { + // generate a 128 bytes vec with random data + let mut rng = rand::rngs::StdRng::from_entropy(); + let mut buf = vec![0u8; buf_len as usize]; + rng.fill(&mut buf[..]); + new_udp_packet( + |header| { + header.msg_type = UdpPacketType::HolePunch as u8; + header.conn_id.set(tid); + header.len.set(buf_len); + }, + Some(&mut buf), + ) +} + +fn is_stun_packet(b: &[u8]) -> bool { + // stun has following pattern: + // 1. first two bits are 0b00 + // 2. magic cookie between 32-64 bits: 0x2112A442 + b[4..8] == [0x21, 0x12, 0xA4, 0x42] && b[0] & 0xC0 == 0 +} + +async fn respond_stun_packet( + socket: Arc, + addr: SocketAddr, + req_buf: Vec, +) -> Result<(), anyhow::Error> { + use crate::common::stun_codec_ext::*; + use bytecodec::DecodeExt as _; + use bytecodec::EncodeExt as _; + use stun_codec::rfc5389::attributes::MappedAddress; + use stun_codec::rfc5389::methods::BINDING; + use stun_codec::{Message, MessageClass, MessageDecoder, MessageEncoder}; + + let mut decoder = MessageDecoder::::new(); + let req_msg = decoder + .decode_from_bytes(&req_buf) + .map_err(|e| anyhow::anyhow!("stun decode error: {:?}", e))? + .map_err(|e| anyhow::anyhow!("stun decode broken message error: {:?}", e))?; + + let tid = req_msg.transaction_id(); + // we only respond easytier stun req, whose tid has 0xdeadbeef prefix + if tid.as_bytes()[0..4] != [0xde, 0xad, 0xbe, 0xef] { + anyhow::bail!("stun req tid not from easytier"); + } + + let mut resp_msg = Message::::new( + MessageClass::SuccessResponse, + BINDING, + // we discard the prefix, make sure our implementation is not compatible with other stun client + u32_to_tid(tid_to_u32(&tid)), + ); + resp_msg.add_attribute(Attribute::MappedAddress(MappedAddress::new(addr.clone()))); + + let mut encoder = MessageEncoder::new(); + let rsp_buf = encoder + .encode_into_bytes(resp_msg.clone()) + .map_err(|e| anyhow::anyhow!("stun encode error: {:?}", e))?; + + socket + .send_to(&rsp_buf, addr.clone()) + .await + .with_context(|| "send stun response error")?; + + tracing::debug!(?addr, ?req_msg, "udp respond stun packet done"); + Ok(()) +} + +fn get_zcpacket_from_buf(buf: BytesMut, allow_stun: bool) -> Result { + let dg_size = buf.len(); + if dg_size < UDP_TUNNEL_HEADER_SIZE { + return Err(TunnelError::InvalidPacket(format!( + "udp packet size too small: {:?}, packet: {:?}", + dg_size, buf + ))); + } + + if allow_stun && is_stun_packet(&buf[..UDP_TUNNEL_HEADER_SIZE]) { + return Ok(ZCPacket::new_from_buf(buf, ZCPacketType::UDP)); + } + + let zc_packet = ZCPacket::new_from_buf(buf, ZCPacketType::UDP); + let header = zc_packet.udp_tunnel_header().unwrap(); + let payload_len = header.len.get() as usize; + if payload_len != dg_size - UDP_TUNNEL_HEADER_SIZE { + return Err(TunnelError::InvalidPacket(format!( + "udp packet payload len not match: header len: {:?}, real len: {:?}", + payload_len, dg_size + ))); + } + + Ok(zc_packet) +} + +#[instrument] +async fn forward_from_ring_to_udp( + mut ring_recv: RingStream, + socket: &Arc, + addr: &SocketAddr, + conn_id: u32, +) -> Option { + tracing::debug!("udp forward from ring to udp"); + loop { + let Some(buf) = ring_recv.next().await else { + return None; + }; + let packet = match buf { + Ok(v) => v, + Err(e) => { + return Some(e); + } + }; + + let mut packet = packet.convert_type(ZCPacketType::UDP); + let udp_payload_len = packet.udp_payload().len(); + let header = packet.mut_udp_tunnel_header().unwrap(); + header.conn_id.set(conn_id); + header.len.set(udp_payload_len as u16); + header.msg_type = UdpPacketType::Data as u8; + + let buf = packet.into_bytes(); + tracing::trace!(?udp_payload_len, ?buf, "udp forward from ring to udp"); + let ret = socket.send_to(&buf, &addr).await; + if ret.is_err() { + return Some(TunnelError::IOError(ret.unwrap_err())); + } else if ret.unwrap() == 0 { + return None; + } + } +} + +async fn udp_recv_from_socket_forward_task(socket: Arc, allow_stun: bool, mut f: F) +where + F: FnMut(ZCPacket, SocketAddr) -> (), +{ + let mut buf = BytesMut::new(); + loop { + reserve_buf(&mut buf, UDP_DATA_MTU, UDP_DATA_MTU * 16); + let (dg_size, addr) = match socket.recv_buf_from(&mut buf).await { + Ok(v) => v, + Err(e) => { + tracing::error!(?e, "udp recv from socket error"); + break; + } + }; + tracing::trace!( + "udp recv packet: {:?}, buf: {:?}, size: {}", + addr, + buf, + dg_size + ); + + let zc_packet = match get_zcpacket_from_buf(buf.split(), allow_stun) { + Ok(v) => v, + Err(e) => { + tracing::warn!(?e, "udp get zc packet from buf error"); + continue; + } + }; + + f(zc_packet, addr); + } +} + +struct UdpConnection { + socket: Arc, + conn_id: u32, + dst_addr: SocketAddr, + + ring_sender: RingSink, + forward_task: ScopedTask<()>, +} + +impl UdpConnection { + pub fn new( + socket: Arc, + conn_id: u32, + dst_addr: SocketAddr, + ring_sender: RingSink, + ring_recv: RingStream, + close_event_sender: UdpCloseEventSender, + ) -> Self { + let s = socket.clone(); + let forward_task = tokio::spawn(async move { + let close_event_sender = close_event_sender; + let err = forward_from_ring_to_udp(ring_recv, &s, &dst_addr, conn_id).await; + if let Err(e) = close_event_sender.send((dst_addr, err)) { + tracing::error!(?e, "udp send close event error"); + } + }) + .into(); + + Self { + socket, + conn_id, + dst_addr, + ring_sender, + forward_task, + } + } + + pub fn handle_packet_from_remote(&mut self, zc_packet: ZCPacket) -> Result<(), TunnelError> { + let header = zc_packet.udp_tunnel_header().unwrap(); + let conn_id = header.conn_id.get(); + + if header.msg_type != UdpPacketType::Data as u8 { + return Err(TunnelError::InvalidPacket("not data packet".to_owned())); + } + + if self.conn_id != conn_id { + return Err(TunnelError::ConnIdNotMatch(self.conn_id, conn_id)); + } + + if zc_packet.is_lossy() { + if let Err(e) = self.ring_sender.try_send(zc_packet) { + tracing::trace!(?e, "ring sender full, drop lossy packet"); + } + } else { + if let Err(e) = self.ring_sender.force_send(zc_packet) { + tracing::trace!(?e, "ring sender full, drop non-lossy packet"); + } + } + + Ok(()) + } +} + +#[derive(Clone)] +struct UdpTunnelListenerData { + local_url: url::Url, + socket: Option>, + sock_map: Arc>, + conn_send: Sender>, + close_event_sender: UdpCloseEventSender, +} + +impl UdpTunnelListenerData { + pub fn new( + local_url: url::Url, + conn_send: Sender>, + close_event_sender: UdpCloseEventSender, + ) -> Self { + Self { + local_url, + socket: None, + sock_map: Arc::new(DashMap::new()), + conn_send, + close_event_sender, + } + } + + async fn handle_new_connect(self: Self, remote_addr: SocketAddr, zc_packet: ZCPacket) { + let udp_payload = zc_packet.udp_payload(); + if udp_payload.len() != 8 { + tracing::warn!( + "udp syn packet payload len not match: {:?}, packet: {:?}", + udp_payload.len(), + zc_packet, + ); + return; + } + let magic = u64::from_le_bytes(udp_payload[..8].try_into().unwrap()); + let conn_id = zc_packet.udp_tunnel_header().unwrap().conn_id.get(); + + tracing::info!(?conn_id, ?remote_addr, "udp connection accept handling",); + let socket = self.socket.as_ref().unwrap().clone(); + + let sack_buf = new_sack_packet(conn_id, magic).into_bytes(); + if let Err(e) = socket.send_to(&sack_buf, remote_addr).await { + tracing::error!(?e, "udp send sack packet error"); + return; + } + + let ring_for_send_udp = Arc::new(RingTunnel::new(128)); + let ring_for_recv_udp = Arc::new(RingTunnel::new(128)); + tracing::debug!( + ?ring_for_send_udp, + ?ring_for_recv_udp, + "udp build tunnel for listener" + ); + + let internal_conn = UdpConnection::new( + socket.clone(), + conn_id, + remote_addr, + RingSink::new(ring_for_recv_udp.clone()), + RingStream::new(ring_for_send_udp.clone()), + self.close_event_sender.clone(), + ); + self.sock_map.insert(remote_addr, internal_conn); + + let conn = Box::new(TunnelWrapper::new( + Box::new(RingStream::new(ring_for_recv_udp)), + Box::new(RingSink::new(ring_for_send_udp)), + Some(TunnelInfo { + tunnel_type: "udp".to_owned(), + local_addr: Some(self.local_url.clone().into()), + remote_addr: Some( + build_url_from_socket_addr(&remote_addr.to_string(), "udp").into(), + ), + }), + )); + + tracing::info!(info = ?conn.info().unwrap().remote_addr, "udp connection accept done"); + + if let Err(e) = self.conn_send.send(conn).await { + tracing::warn!(?e, "udp send conn to accept channel error"); + } + } + + fn do_forward_one_packet_to_conn(&self, zc_packet: ZCPacket, addr: SocketAddr) { + let header = zc_packet.udp_tunnel_header().unwrap(); + if header.msg_type == UdpPacketType::Syn as u8 { + tokio::spawn(Self::handle_new_connect(self.clone(), addr, zc_packet)); + } else if is_stun_packet(header.as_bytes()) { + // ignore stun packet + tracing::debug!("udp forward packet ignore stun packet"); + let socket = self.socket.as_ref().unwrap().clone(); + tokio::spawn(async move { + let ret = respond_stun_packet(socket, addr, zc_packet.inner().to_vec()).await; + if let Err(e) = ret { + tracing::error!(?e, "udp respond stun packet error"); + } + }); + } else if header.msg_type != UdpPacketType::HolePunch as u8 { + let Some(mut conn) = self.sock_map.get_mut(&addr) else { + tracing::trace!(?header, "udp forward packet error, connection not found"); + return; + }; + if let Err(e) = conn.handle_packet_from_remote(zc_packet) { + tracing::trace!(?e, "udp forward packet error"); + } + } + } + + async fn do_forward_task(self: Self) { + let socket = self.socket.as_ref().unwrap().clone(); + udp_recv_from_socket_forward_task(socket, true, |zc_packet, addr| { + self.do_forward_one_packet_to_conn(zc_packet, addr); + }) + .await; + } +} + +pub struct UdpTunnelListener { + addr: url::Url, + socket: Option>, + + conn_recv: Receiver>, + data: UdpTunnelListenerData, + forward_tasks: Arc>>, + close_event_recv: Option, +} + +impl UdpTunnelListener { + pub fn new(addr: url::Url) -> Self { + let (close_event_send, close_event_recv) = tokio::sync::mpsc::unbounded_channel(); + let (conn_send, conn_recv) = tokio::sync::mpsc::channel(100); + Self { + addr: addr.clone(), + socket: None, + conn_recv, + data: UdpTunnelListenerData::new(addr, conn_send, close_event_send), + forward_tasks: Arc::new(std::sync::Mutex::new(JoinSet::new())), + close_event_recv: Some(close_event_recv), + } + } + + pub fn get_socket(&self) -> Option> { + self.socket.clone() + } +} + +#[async_trait] +impl TunnelListener for UdpTunnelListener { + async fn listen(&mut self) -> Result<(), super::TunnelError> { + let addr = super::check_scheme_and_get_socket_addr::(&self.addr, "udp")?; + + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(addr), + socket2::Type::DGRAM, + Some(socket2::Protocol::UDP), + )?; + + let tunnel_url: TunnelUrl = self.addr.clone().into(); + if let Some(bind_dev) = tunnel_url.bind_dev() { + setup_sokcet2_ext(&socket2_socket, &addr, Some(bind_dev))?; + } else { + setup_sokcet2(&socket2_socket, &addr)?; + } + + self.socket = Some(Arc::new(UdpSocket::from_std(socket2_socket.into())?)); + self.data.socket = self.socket.clone(); + + self.addr + .set_port(Some(self.socket.as_ref().unwrap().local_addr()?.port())) + .unwrap(); + + self.forward_tasks + .lock() + .unwrap() + .spawn(self.data.clone().do_forward_task()); + + let sock_map = Arc::downgrade(&self.data.sock_map.clone()); + let mut close_recv = self.close_event_recv.take().unwrap(); + self.forward_tasks.lock().unwrap().spawn(async move { + while let Some((dst_addr, err)) = close_recv.recv().await { + if let Some(err) = err { + tracing::error!(?err, "udp close event error"); + } + sock_map.upgrade().map(|v| v.remove(&dst_addr)); + } + }); + + join_joinset_background(self.forward_tasks.clone(), "UdpTunnelListener".to_owned()); + + Ok(()) + } + + async fn accept(&mut self) -> Result, super::TunnelError> { + tracing::info!("start udp accept: {:?}", self.addr); + while let Some(conn) = self.conn_recv.recv().await { + return Ok(conn); + } + return Err(super::TunnelError::InternalError( + "udp accept error".to_owned(), + )); + } + + fn local_url(&self) -> url::Url { + self.addr.clone() + } + + fn get_conn_counter(&self) -> Arc> { + struct UdpTunnelConnCounter { + sock_map: Weak>, + } + + impl TunnelConnCounter for UdpTunnelConnCounter { + fn get(&self) -> Option { + self.sock_map.upgrade().map(|x| x.len() as u32) + } + } + + impl Debug for UdpTunnelConnCounter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UdpTunnelConnCounter") + .field("sock_map_len", &self.get()) + .finish() + } + } + + Arc::new(Box::new(UdpTunnelConnCounter { + sock_map: Arc::downgrade(&self.data.sock_map.clone()), + })) + } +} + +#[derive(Debug)] +pub struct UdpTunnelConnector { + addr: url::Url, + bind_addrs: Vec, + ip_version: IpVersion, +} + +impl UdpTunnelConnector { + pub fn new(addr: url::Url) -> Self { + Self { + addr, + bind_addrs: vec![], + ip_version: IpVersion::Both, + } + } + + async fn wait_sack( + socket: &UdpSocket, + addr: SocketAddr, + conn_id: u32, + magic: u64, + ) -> Result { + let mut buf = BytesMut::new(); + buf.reserve(UDP_DATA_MTU); + + let (usize, recv_addr) = tokio::time::timeout( + tokio::time::Duration::from_secs(3), + socket.recv_buf_from(&mut buf), + ) + .await??; + let zc_packet = get_zcpacket_from_buf(buf.split(), false)?; + if recv_addr != addr { + tracing::warn!(?recv_addr, ?addr, ?usize, "udp wait sack addr not match"); + } + + let header = zc_packet.udp_tunnel_header().unwrap(); + + if header.conn_id.get() != conn_id { + return Err(super::TunnelError::ConnIdNotMatch( + header.conn_id.get(), + conn_id, + )); + } + + if header.msg_type != UdpPacketType::Sack as u8 { + return Err(TunnelError::InvalidPacket("not sack packet".to_owned())); + } + + let payload = zc_packet.udp_payload(); + if payload.len() != 8 { + return Err(TunnelError::InvalidPacket( + "udp sack packet payload len not match".to_owned(), + )); + } + + let sack_magic = u64::from_le_bytes(payload[..8].try_into().unwrap()); + if sack_magic != magic { + return Err(TunnelError::InvalidPacket( + "udp sack magic not match".to_owned(), + )); + } + + Ok(recv_addr) + } + + async fn wait_sack_loop( + socket: &UdpSocket, + addr: SocketAddr, + conn_id: u32, + magic: u64, + ) -> Result { + loop { + let ret = Self::wait_sack(socket, addr, conn_id, magic).await; + if ret.is_err() { + tracing::debug!(?ret, "udp wait sack error"); + continue; + } else { + return ret; + } + } + } + + async fn build_tunnel( + &self, + socket: Arc, + dst_addr: SocketAddr, + conn_id: u32, + ) -> Result, super::TunnelError> { + let ring_for_send_udp = Arc::new(RingTunnel::new(128)); + let ring_for_recv_udp = Arc::new(RingTunnel::new(128)); + tracing::debug!( + ?ring_for_send_udp, + ?ring_for_recv_udp, + "udp build tunnel for connector" + ); + + let (close_event_sender, mut close_event_recv) = tokio::sync::mpsc::unbounded_channel(); + + let ring_recv = RingStream::new(ring_for_send_udp.clone()); + let ring_sender = RingSink::new(ring_for_recv_udp.clone()); + let mut udp_conn = UdpConnection::new( + socket.clone(), + conn_id, + dst_addr, + ring_sender, + ring_recv, + close_event_sender, + ); + + let socket_clone = socket.clone(); + tokio::spawn( + async move { + tokio::select! { + _ = close_event_recv.recv() => { + tracing::debug!("connector udp close event"); + return; + } + _ = udp_recv_from_socket_forward_task(socket_clone,false, |zc_packet, addr| { + tracing::trace!(?addr, "connector udp forward task done"); + if let Err(e) = udp_conn.handle_packet_from_remote(zc_packet) { + tracing::trace!(?e, ?addr, "udp forward packet error"); + } + }) => { + tracing::debug!("connector udp forward task done"); + return; + } + } + } + .instrument(tracing::info_span!( + "udp forward from udp to ring", + ?conn_id, + ?dst_addr, + )), + ); + + Ok(Box::new(TunnelWrapper::new( + Box::new(RingStream::new(ring_for_recv_udp)), + Box::new(RingSink::new(ring_for_send_udp)), + Some(TunnelInfo { + tunnel_type: "udp".to_owned(), + local_addr: Some( + build_url_from_socket_addr(&socket.local_addr()?.to_string(), "udp").into(), + ), + remote_addr: Some(self.addr.clone().into()), + }), + ))) + } + + pub async fn try_connect_with_socket( + &self, + socket: Arc, + addr: SocketAddr, + ) -> Result, super::TunnelError> { + tracing::warn!("udp connect: {:?}", self.addr); + + #[cfg(target_os = "windows")] + crate::arch::windows::disable_connection_reset(socket.as_ref())?; + + // send syn + let conn_id = rand::random(); + let magic = rand::random(); + let udp_packet = new_syn_packet(conn_id, magic).into_bytes(); + let ret = socket.send_to(&udp_packet, &addr).await?; + tracing::warn!(?udp_packet, ?ret, "udp send syn"); + + // wait sack + let recv_addr = tokio::time::timeout( + tokio::time::Duration::from_secs(3), + Self::wait_sack_loop(&socket, addr, conn_id, magic), + ) + .await??; + + if recv_addr != addr { + tracing::debug!(?recv_addr, ?addr, "udp connect addr not match"); + } + + self.build_tunnel(socket, addr, conn_id).await + } + + async fn connect_with_default_bind( + &mut self, + addr: SocketAddr, + ) -> Result, super::TunnelError> { + let socket = if addr.is_ipv4() { + UdpSocket::bind("0.0.0.0:0").await? + } else { + UdpSocket::bind("[::]:0").await? + }; + + return self.try_connect_with_socket(Arc::new(socket), addr).await; + } + + async fn connect_with_custom_bind( + &mut self, + addr: SocketAddr, + ) -> Result, super::TunnelError> { + let futures = FuturesUnordered::new(); + + for bind_addr in self.bind_addrs.iter() { + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(*bind_addr), + socket2::Type::DGRAM, + Some(socket2::Protocol::UDP), + )?; + if let Err(e) = setup_sokcet2(&socket2_socket, bind_addr) { + tracing::error!(bind_addr = ?bind_addr, ?addr, "bind addr fail: {:?}", e); + continue; + } + let socket = UdpSocket::from_std(socket2_socket.into())?; + futures.push(self.try_connect_with_socket(Arc::new(socket), addr)); + } + wait_for_connect_futures(futures).await + } +} + +#[async_trait] +impl super::TunnelConnector for UdpTunnelConnector { + async fn connect(&mut self) -> Result, super::TunnelError> { + let addr = super::check_scheme_and_get_socket_addr_ext::( + &self.addr, + "udp", + self.ip_version, + )?; + if self.bind_addrs.is_empty() || addr.is_ipv6() { + self.connect_with_default_bind(addr).await + } else { + self.connect_with_custom_bind(addr).await + } + } + + fn remote_url(&self) -> url::Url { + self.addr.clone() + } + + fn set_bind_addrs(&mut self, addrs: Vec) { + self.bind_addrs = addrs; + } + + fn set_ip_version(&mut self, ip_version: IpVersion) { + self.ip_version = ip_version; + } +} + +#[cfg(test)] +mod tests { + use std::{net::IpAddr, time::Duration}; + + use futures::SinkExt; + use tokio::time::timeout; + + use super::*; + use crate::{ + common::global_ctx::tests::get_mock_global_ctx, + tunnel::{ + check_scheme_and_get_socket_addr, + common::{ + get_interface_name_by_ip, + tests::{_tunnel_bench, _tunnel_echo_server, _tunnel_pingpong, wait_for_condition}, + }, + TunnelConnector, + }, + }; + + #[tokio::test] + async fn udp_pingpong() { + let listener = UdpTunnelListener::new("udp://0.0.0.0:5556".parse().unwrap()); + let connector = UdpTunnelConnector::new("udp://127.0.0.1:5556".parse().unwrap()); + _tunnel_pingpong(listener, connector).await; + } + + #[tokio::test] + async fn udp_bench() { + let listener = UdpTunnelListener::new("udp://0.0.0.0:5555".parse().unwrap()); + let connector = UdpTunnelConnector::new("udp://127.0.0.1:5555".parse().unwrap()); + _tunnel_bench(listener, connector).await + } + + #[tokio::test] + async fn udp_bench_with_bind() { + let listener = UdpTunnelListener::new("udp://127.0.0.1:5554".parse().unwrap()); + let mut connector = UdpTunnelConnector::new("udp://127.0.0.1:5554".parse().unwrap()); + connector.set_bind_addrs(vec!["127.0.0.1:0".parse().unwrap()]); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + #[should_panic] + async fn udp_bench_with_bind_fail() { + let listener = UdpTunnelListener::new("udp://127.0.0.1:5553".parse().unwrap()); + let mut connector = UdpTunnelConnector::new("udp://127.0.0.1:5553".parse().unwrap()); + connector.set_bind_addrs(vec!["10.0.0.1:0".parse().unwrap()]); + _tunnel_pingpong(listener, connector).await + } + + async fn send_random_data_to_socket(remote_url: url::Url) { + let socket = UdpSocket::bind("0.0.0.0:0").await.unwrap(); + socket + .connect(format!( + "{}:{}", + remote_url.host().unwrap(), + remote_url.port().unwrap() + )) + .await + .unwrap(); + + // get a random 100-len buf + loop { + let mut buf = vec![0u8; 100]; + rand::thread_rng().fill(&mut buf[..]); + socket.send(&buf).await.unwrap(); + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + } + } + + #[tokio::test] + async fn udp_multiple_conns() { + let mut listener = UdpTunnelListener::new("udp://0.0.0.0:5557".parse().unwrap()); + listener.listen().await.unwrap(); + + let _lis = tokio::spawn(async move { + loop { + let ret = listener.accept().await.unwrap(); + assert_eq!( + ret.info() + .unwrap() + .local_addr + .unwrap_or_default() + .to_string(), + listener.local_url().to_string() + ); + tokio::spawn(async move { _tunnel_echo_server(ret, false).await }); + } + }); + + let mut connector1 = UdpTunnelConnector::new("udp://127.0.0.1:5557".parse().unwrap()); + let mut connector2 = UdpTunnelConnector::new("udp://127.0.0.1:5557".parse().unwrap()); + + let t1 = connector1.connect().await.unwrap(); + let t2 = connector2.connect().await.unwrap(); + + tokio::spawn(timeout( + Duration::from_secs(2), + send_random_data_to_socket(t1.info().unwrap().local_addr.unwrap().into()), + )); + tokio::spawn(timeout( + Duration::from_secs(2), + send_random_data_to_socket(t1.info().unwrap().remote_addr.unwrap().into()), + )); + tokio::spawn(timeout( + Duration::from_secs(2), + send_random_data_to_socket(t2.info().unwrap().remote_addr.unwrap().into()), + )); + + let sender1 = tokio::spawn(async move { + let (mut stream, mut sink) = t1.split(); + + for i in 0..10 { + sink.send(ZCPacket::new_with_payload("hello1".as_bytes())) + .await + .unwrap(); + let recv = stream.next().await.unwrap().unwrap(); + println!("t1 recv: {:?}, {:?}", recv, i); + assert_eq!(recv.payload(), "hello1".as_bytes()); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + }); + + let sender2 = tokio::spawn(async move { + let (mut stream, mut sink) = t2.split(); + + for i in 0..10 { + sink.send(ZCPacket::new_with_payload("hello2".as_bytes())) + .await + .unwrap(); + let recv = stream.next().await.unwrap().unwrap(); + println!("t2 recv: {:?}, {:?}", recv, i); + assert_eq!(recv.payload(), "hello2".as_bytes()); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + }); + + let _ = tokio::join!(sender1, sender2); + } + + #[tokio::test] + async fn bind_multi_ip_to_same_dev() { + let global_ctx = get_mock_global_ctx(); + let ips = global_ctx + .get_ip_collector() + .collect_ip_addrs() + .await + .interface_ipv4s; + if ips.is_empty() { + return; + } + let bind_dev = get_interface_name_by_ip(&IpAddr::V4(ips[0].into())); + + for ip in ips { + println!("bind to ip: {:?}, {:?}", ip, bind_dev); + let addr = check_scheme_and_get_socket_addr::( + &format!("udp://{}:11111", ip.to_string()).parse().unwrap(), + "udp", + ) + .unwrap(); + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(addr), + socket2::Type::DGRAM, + Some(socket2::Protocol::UDP), + ) + .unwrap(); + setup_sokcet2_ext(&socket2_socket, &addr, bind_dev.clone()).unwrap(); + } + } + + #[tokio::test] + async fn bind_same_port() { + println!("{}", "[::]:8888".parse::().unwrap()); + let mut listener = UdpTunnelListener::new("udp://[::]:31014".parse().unwrap()); + let mut listener2 = UdpTunnelListener::new("udp://0.0.0.0:31014".parse().unwrap()); + listener.listen().await.unwrap(); + listener2.listen().await.unwrap(); + } + + #[tokio::test] + async fn ipv6_pingpong() { + let listener = UdpTunnelListener::new("udp://[::1]:31015".parse().unwrap()); + let connector = UdpTunnelConnector::new("udp://[::1]:31015".parse().unwrap()); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn ipv6_domain_pingpong() { + let listener = UdpTunnelListener::new("udp://[::1]:31016".parse().unwrap()); + let mut connector = + UdpTunnelConnector::new("udp://test.easytier.top:31016".parse().unwrap()); + connector.set_ip_version(IpVersion::V6); + _tunnel_pingpong(listener, connector).await; + + let listener = UdpTunnelListener::new("udp://127.0.0.1:31016".parse().unwrap()); + let mut connector = + UdpTunnelConnector::new("udp://test.easytier.top:31016".parse().unwrap()); + connector.set_ip_version(IpVersion::V4); + _tunnel_pingpong(listener, connector).await; + } + + #[tokio::test] + async fn test_alloc_port() { + // v4 + let mut listener = UdpTunnelListener::new("udp://0.0.0.0:0".parse().unwrap()); + listener.listen().await.unwrap(); + let port = listener.local_url().port().unwrap(); + assert!(port > 0); + + // v6 + let mut listener = UdpTunnelListener::new("udp://[::]:0".parse().unwrap()); + listener.listen().await.unwrap(); + let port = listener.local_url().port().unwrap(); + assert!(port > 0); + } + + #[tokio::test] + async fn test_conn_counter() { + let mut listener = UdpTunnelListener::new("udp://0.0.0.0:5556".parse().unwrap()); + let mut connector = UdpTunnelConnector::new("udp://127.0.0.1:5556".parse().unwrap()); + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + let _c1 = connector.connect().await.unwrap(); + let _c2 = connector.connect().await.unwrap(); + }); + + let conn_counter = listener.get_conn_counter(); + + listener.listen().await.unwrap(); + let c1 = listener.accept().await.unwrap(); + assert_eq!(conn_counter.get(), Some(1)); + let c2 = listener.accept().await.unwrap(); + assert_eq!(conn_counter.get(), Some(2)); + + drop(c2); + wait_for_condition( + || async { conn_counter.get() == Some(1) }, + Duration::from_secs(1), + ) + .await; + + drop(c1); + wait_for_condition( + || async { conn_counter.get().unwrap_or(0) == 0 }, + Duration::from_secs(1), + ) + .await; + } +} diff --git a/rust/easytier/src/tunnel/websocket.rs b/rust/easytier/src/tunnel/websocket.rs new file mode 100644 index 0000000..8214c65 --- /dev/null +++ b/rust/easytier/src/tunnel/websocket.rs @@ -0,0 +1,344 @@ +use std::{net::SocketAddr, sync::Arc, time::Duration}; + +use anyhow::Context; +use bytes::BytesMut; +use futures::{stream::FuturesUnordered, SinkExt, StreamExt}; +use tokio::{ + net::{TcpListener, TcpSocket, TcpStream}, + time::timeout, +}; +use tokio_rustls::TlsAcceptor; +use tokio_websockets::{ClientBuilder, Limits, MaybeTlsStream, Message}; +use zerocopy::AsBytes; + +use super::TunnelInfo; +use crate::tunnel::insecure_tls::get_insecure_tls_client_config; + +use super::{ + common::{setup_sokcet2, wait_for_connect_futures, TunnelWrapper}, + insecure_tls::{get_insecure_tls_cert, init_crypto_provider}, + packet_def::{ZCPacket, ZCPacketType}, + FromUrl, IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelListener, +}; + +fn is_wss(addr: &url::Url) -> Result { + match addr.scheme() { + "ws" => Ok(false), + "wss" => Ok(true), + _ => Err(TunnelError::InvalidProtocol(addr.scheme().to_string())), + } +} + +async fn sink_from_zc_packet(msg: ZCPacket) -> Result { + Ok(Message::binary(msg.tunnel_payload_bytes().freeze())) +} + +async fn map_from_ws_message( + msg: Result, +) -> Option> { + if msg.is_err() { + tracing::error!(?msg, "recv from websocket error"); + return Some(Err(TunnelError::WebSocketError(msg.unwrap_err()))); + } + + let msg = msg.unwrap(); + if msg.is_close() { + tracing::warn!("recv close message from websocket"); + return None; + } + + if !msg.is_binary() { + let msg = format!("{:?}", msg); + tracing::error!(?msg, "Invalid packet"); + return Some(Err(TunnelError::InvalidPacket(msg))); + } + + Some(Ok(ZCPacket::new_from_buf( + BytesMut::from(msg.into_payload().as_bytes()), + ZCPacketType::DummyTunnel, + ))) +} + +#[derive(Debug)] +pub struct WSTunnelListener { + addr: url::Url, + listener: Option, +} + +impl WSTunnelListener { + pub fn new(addr: url::Url) -> Self { + WSTunnelListener { + addr, + listener: None, + } + } + + async fn try_accept(&mut self, stream: TcpStream) -> Result, TunnelError> { + let info = TunnelInfo { + tunnel_type: self.addr.scheme().to_owned(), + local_addr: Some(self.local_url().into()), + remote_addr: Some( + super::build_url_from_socket_addr( + &stream.peer_addr()?.to_string(), + self.addr.scheme().to_string().as_str(), + ) + .into(), + ), + }; + + let server_bulder = tokio_websockets::ServerBuilder::new().limits(Limits::unlimited()); + + let ret: Box = if is_wss(&self.addr)? { + init_crypto_provider(); + let (certs, key) = get_insecure_tls_cert(); + let config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, key) + .with_context(|| "Failed to create server config")?; + let acceptor = TlsAcceptor::from(Arc::new(config)); + + let stream = acceptor.accept(stream).await?; + let (write, read) = server_bulder.accept(stream).await?.split(); + + Box::new(TunnelWrapper::new( + read.filter_map(move |msg| map_from_ws_message(msg)), + write.with(move |msg| sink_from_zc_packet(msg)), + Some(info), + )) + } else { + let (write, read) = server_bulder.accept(stream).await?.split(); + Box::new(TunnelWrapper::new( + read.filter_map(move |msg| map_from_ws_message(msg)), + write.with(move |msg| sink_from_zc_packet(msg)), + Some(info), + )) + }; + + Ok(ret) + } +} + +#[async_trait::async_trait] +impl TunnelListener for WSTunnelListener { + async fn listen(&mut self) -> Result<(), TunnelError> { + let addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both)?; + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(addr), + socket2::Type::STREAM, + Some(socket2::Protocol::TCP), + )?; + setup_sokcet2(&socket2_socket, &addr)?; + let socket = TcpSocket::from_std_stream(socket2_socket.into()); + + self.addr + .set_port(Some(socket.local_addr()?.port())) + .unwrap(); + + self.listener = Some(socket.listen(1024)?); + Ok(()) + } + + async fn accept(&mut self) -> Result, super::TunnelError> { + loop { + let listener = self.listener.as_ref().unwrap(); + // only fail on tcp accept error + let (stream, _) = listener.accept().await?; + stream.set_nodelay(true).unwrap(); + match timeout(Duration::from_secs(3), self.try_accept(stream)).await { + Ok(Ok(tunnel)) => return Ok(tunnel), + e => { + tracing::error!(?e, ?self, "Failed to accept ws/wss tunnel"); + continue; + } + } + } + } + + fn local_url(&self) -> url::Url { + self.addr.clone() + } +} + +pub struct WSTunnelConnector { + addr: url::Url, + ip_version: IpVersion, + + bind_addrs: Vec, +} + +impl WSTunnelConnector { + pub fn new(addr: url::Url) -> Self { + WSTunnelConnector { + addr, + ip_version: IpVersion::Both, + + bind_addrs: vec![], + } + } + + async fn connect_with( + addr: url::Url, + ip_version: IpVersion, + tcp_socket: TcpSocket, + ) -> Result, TunnelError> { + let is_wss = is_wss(&addr)?; + let socket_addr = SocketAddr::from_url(addr.clone(), ip_version)?; + let host = socket_addr.ip(); + let stream = tcp_socket.connect(socket_addr).await?; + + let info = TunnelInfo { + tunnel_type: addr.scheme().to_owned(), + local_addr: Some( + super::build_url_from_socket_addr( + &stream.local_addr()?.to_string(), + addr.scheme().to_string().as_str(), + ) + .into(), + ), + remote_addr: Some(addr.clone().into()), + }; + + let c = ClientBuilder::from_uri(http::Uri::try_from(addr.to_string()).unwrap()); + let stream: MaybeTlsStream = if is_wss { + init_crypto_provider(); + let tls_conn = + tokio_rustls::TlsConnector::from(Arc::new(get_insecure_tls_client_config())); + let stream = tls_conn + .connect(host.to_string().try_into().unwrap(), stream) + .await?; + MaybeTlsStream::Rustls(stream) + } else { + MaybeTlsStream::Plain(stream) + }; + + let (client, _) = c.connect_on(stream).await?; + let (write, read) = client.split(); + let read = read.filter_map(move |msg| map_from_ws_message(msg)); + let write = write.with(move |msg| sink_from_zc_packet(msg)); + Ok(Box::new(TunnelWrapper::new(read, write, Some(info)))) + } + + async fn connect_with_default_bind( + &mut self, + addr: SocketAddr, + ) -> Result, super::TunnelError> { + let socket = if addr.is_ipv4() { + TcpSocket::new_v4()? + } else { + TcpSocket::new_v6()? + }; + Self::connect_with(self.addr.clone(), self.ip_version, socket).await + } + + async fn connect_with_custom_bind( + &mut self, + addr: SocketAddr, + ) -> Result, super::TunnelError> { + let futures = FuturesUnordered::new(); + + for bind_addr in self.bind_addrs.iter() { + tracing::info!(bind_addr = ?bind_addr, ?addr, "bind addr"); + + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(addr), + socket2::Type::STREAM, + Some(socket2::Protocol::TCP), + )?; + + if let Err(e) = setup_sokcet2(&socket2_socket, bind_addr) { + tracing::error!(bind_addr = ?bind_addr, ?addr, "bind addr fail: {:?}", e); + continue; + } + + let socket = TcpSocket::from_std_stream(socket2_socket.into()); + futures.push(Self::connect_with( + self.addr.clone(), + self.ip_version, + socket, + )) + } + + wait_for_connect_futures(futures).await + } +} + +#[async_trait::async_trait] +impl TunnelConnector for WSTunnelConnector { + async fn connect(&mut self) -> Result, super::TunnelError> { + let addr = SocketAddr::from_url(self.addr.clone(), self.ip_version)?; + if self.bind_addrs.is_empty() || addr.is_ipv6() { + self.connect_with_default_bind(addr).await + } else { + self.connect_with_custom_bind(addr).await + } + } + + fn remote_url(&self) -> url::Url { + self.addr.clone() + } + + fn set_ip_version(&mut self, ip_version: IpVersion) { + self.ip_version = ip_version; + } + + fn set_bind_addrs(&mut self, addrs: Vec) { + self.bind_addrs = addrs; + } +} + +#[cfg(test)] +pub mod tests { + use crate::tunnel::common::tests::_tunnel_pingpong; + use crate::tunnel::websocket::{WSTunnelConnector, WSTunnelListener}; + use crate::tunnel::{TunnelConnector, TunnelListener}; + + #[rstest::rstest] + #[tokio::test] + #[serial_test::serial] + async fn ws_pingpong(#[values("ws", "wss")] proto: &str) { + let listener = WSTunnelListener::new(format!("{}://0.0.0.0:25556", proto).parse().unwrap()); + let connector = + WSTunnelConnector::new(format!("{}://127.0.0.1:25556", proto).parse().unwrap()); + _tunnel_pingpong(listener, connector).await + } + + #[rstest::rstest] + #[tokio::test] + #[serial_test::serial] + async fn ws_pingpong_bind(#[values("ws", "wss")] proto: &str) { + let listener = WSTunnelListener::new(format!("{}://0.0.0.0:25557", proto).parse().unwrap()); + let mut connector = + WSTunnelConnector::new(format!("{}://127.0.0.1:25557", proto).parse().unwrap()); + connector.set_bind_addrs(vec!["127.0.0.1:0".parse().unwrap()]); + _tunnel_pingpong(listener, connector).await + } + + // TODO: tokio-websockets cannot correctly handle close, benchmark case is disabled + // #[rstest::rstest] + // #[tokio::test] + // #[serial_test::serial] + // async fn ws_bench(#[values("ws", "wss")] proto: &str) { + // enable_log(); + // let listener = WSTunnelListener::new(format!("{}://0.0.0.0:25557", proto).parse().unwrap()); + // let connector = + // WSTunnelConnector::new(format!("{}://127.0.0.1:25557", proto).parse().unwrap()); + // _tunnel_bench(listener, connector).await + // } + + #[tokio::test] + async fn ws_accept_wss() { + let mut listener = WSTunnelListener::new("wss://0.0.0.0:25558".parse().unwrap()); + listener.listen().await.unwrap(); + let j = tokio::spawn(async move { + let _ = listener.accept().await; + }); + + let mut connector = WSTunnelConnector::new("ws://127.0.0.1:25558".parse().unwrap()); + connector.connect().await.unwrap_err(); + + let mut connector = WSTunnelConnector::new("wss://127.0.0.1:25558".parse().unwrap()); + connector.connect().await.unwrap(); + + j.abort(); + } +} diff --git a/rust/easytier/src/tunnel/wireguard.rs b/rust/easytier/src/tunnel/wireguard.rs new file mode 100644 index 0000000..4d126ed --- /dev/null +++ b/rust/easytier/src/tunnel/wireguard.rs @@ -0,0 +1,926 @@ +use std::{ + fmt::{Debug, Formatter}, + net::SocketAddr, + pin::Pin, + sync::{atomic::AtomicBool, Arc}, + time::Duration, +}; + +use anyhow::Context; +use async_recursion::async_recursion; +use async_trait::async_trait; +use boringtun::{ + noise::{errors::WireGuardError, Tunn, TunnResult}, + x25519::{PublicKey, StaticSecret}, +}; +use bytes::BytesMut; +use crossbeam::atomic::AtomicCell; +use dashmap::DashMap; +use futures::{stream::FuturesUnordered, SinkExt, StreamExt}; +use rand::RngCore; +use tokio::{net::UdpSocket, sync::Mutex, task::JoinSet}; + +use super::TunnelInfo; +use crate::tunnel::{ + build_url_from_socket_addr, + common::TunnelWrapper, + packet_def::{ZCPacket, WG_TUNNEL_HEADER_SIZE}, +}; + +use super::{ + check_scheme_and_get_socket_addr, + common::{setup_sokcet2, setup_sokcet2_ext, wait_for_connect_futures}, + generate_digest_from_str, + packet_def::{ZCPacketType, PEER_MANAGER_HEADER_SIZE}, + ring::create_ring_tunnel_pair, + IpVersion, Tunnel, TunnelError, TunnelListener, TunnelUrl, ZCPacketSink, ZCPacketStream, +}; + +const MAX_PACKET: usize = 2048; + +#[derive(Debug, Clone)] +enum WgType { + // used by easytier peer, need remove/add ip header for in/out wg msg + InternalUse, + // used by wireguard peer, keep original ip header + ExternalUse, +} + +#[derive(Clone)] +pub struct WgConfig { + my_secret_key: StaticSecret, + my_public_key: PublicKey, + + peer_secret_key: StaticSecret, + peer_public_key: PublicKey, + + wg_type: WgType, +} + +impl WgConfig { + pub fn new_from_network_identity(network_name: &str, network_secret: &str) -> Self { + let mut my_sec = [0u8; 32]; + generate_digest_from_str(network_name, network_secret, &mut my_sec); + + let my_secret_key = StaticSecret::from(my_sec); + let my_public_key = PublicKey::from(&my_secret_key); + let peer_secret_key = StaticSecret::from(my_sec); + let peer_public_key = my_public_key.clone(); + + WgConfig { + my_secret_key, + my_public_key, + peer_secret_key, + peer_public_key, + + wg_type: WgType::InternalUse, + } + } + + pub fn new_for_portal(server_key_seed: &str, client_key_seed: &str) -> Self { + let server_cfg = Self::new_from_network_identity("server", server_key_seed); + let client_cfg = Self::new_from_network_identity("client", client_key_seed); + Self { + my_secret_key: server_cfg.my_secret_key, + my_public_key: server_cfg.my_public_key, + peer_secret_key: client_cfg.my_secret_key, + peer_public_key: client_cfg.my_public_key, + + wg_type: WgType::ExternalUse, + } + } + + pub fn my_secret_key(&self) -> &[u8] { + self.my_secret_key.as_bytes() + } + + pub fn peer_secret_key(&self) -> &[u8] { + self.peer_secret_key.as_bytes() + } + + pub fn my_public_key(&self) -> &[u8] { + self.my_public_key.as_bytes() + } + + pub fn peer_public_key(&self) -> &[u8] { + self.peer_public_key.as_bytes() + } +} + +#[derive(Clone)] +struct WgPeerData { + udp: Arc, // only for send + endpoint: SocketAddr, + tunn: Arc>, + wg_type: WgType, + stopped: Arc, +} + +impl Debug for WgPeerData { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WgPeerData") + .field("endpoint", &self.endpoint) + .field("local", &self.udp.local_addr()) + .finish() + } +} + +impl WgPeerData { + #[tracing::instrument] + async fn handle_one_packet_from_me(&self, zc_packet: ZCPacket) -> Result<(), anyhow::Error> { + let mut send_buf = vec![0u8; MAX_PACKET]; + + let packet = if matches!(self.wg_type, WgType::InternalUse) { + let mut zc_packet = zc_packet.convert_type(ZCPacketType::WG); + Self::fill_ip_header(&mut zc_packet); + zc_packet.into_bytes() + } else { + zc_packet.convert_type(ZCPacketType::WG).into_bytes() + }; + tracing::trace!(?packet, "Sending packet to peer"); + + let encapsulate_result = { + let mut peer = self.tunn.lock().await; + peer.encapsulate(&packet, &mut send_buf) + }; + + tracing::trace!( + ?encapsulate_result, + "Received {} bytes from me", + packet.len() + ); + + match encapsulate_result { + TunnResult::WriteToNetwork(packet) => { + self.udp + .send_to(packet, self.endpoint) + .await + .context("Failed to send encrypted IP packet to WireGuard endpoint.")?; + tracing::debug!( + "Sent {} bytes to WireGuard endpoint (encrypted IP packet)", + packet.len() + ); + } + TunnResult::Err(e) => { + tracing::error!("Failed to encapsulate IP packet: {:?}", e); + } + TunnResult::Done => { + // Ignored + } + other => { + tracing::error!( + "Unexpected WireGuard state during encapsulation: {:?}", + other + ); + } + }; + Ok(()) + } + + /// WireGuard consumption task. Receives encrypted packets from the WireGuard endpoint, + /// decapsulates them, and dispatches newly received IP packets. + #[tracing::instrument(skip(sink))] + pub async fn handle_one_packet_from_peer( + &self, + mut sink: S, + recv_buf: &[u8], + ) { + let mut send_buf = vec![0u8; MAX_PACKET]; + let data = &recv_buf[..]; + let decapsulate_result = { + let mut peer = self.tunn.lock().await; + peer.decapsulate(None, data, &mut send_buf) + }; + + tracing::debug!("Decapsulation result: {:?}", decapsulate_result); + + match decapsulate_result { + TunnResult::WriteToNetwork(packet) => { + match self.udp.send_to(packet, self.endpoint).await { + Ok(_) => {} + Err(e) => { + tracing::error!("Failed to send decapsulation-instructed packet to WireGuard endpoint: {:?}", e); + return; + } + }; + let mut peer = self.tunn.lock().await; + loop { + let mut send_buf = vec![0u8; MAX_PACKET]; + match peer.decapsulate(None, &[], &mut send_buf) { + TunnResult::WriteToNetwork(packet) => { + match self.udp.send_to(packet, self.endpoint).await { + Ok(_) => {} + Err(e) => { + tracing::error!("Failed to send decapsulation-instructed packet to WireGuard endpoint: {:?}", e); + break; + } + }; + } + _ => { + break; + } + } + } + } + TunnResult::WriteToTunnelV4(packet, _) | TunnResult::WriteToTunnelV6(packet, _) => { + tracing::debug!( + ?packet, + "receive IP packet from peer: {} bytes", + packet.len() + ); + let mut b = BytesMut::new(); + if matches!(self.wg_type, WgType::InternalUse) { + b.resize(WG_TUNNEL_HEADER_SIZE, 0); + b.extend_from_slice(self.remove_ip_header(packet, packet[0] >> 4 == 4)); + } else { + b.extend_from_slice(packet); + }; + let zc_packet = ZCPacket::new_from_buf(b, ZCPacketType::WG); + tracing::trace!(?zc_packet, "forward zc_packet to sink"); + let ret = sink.send(zc_packet).await; + if ret.is_err() { + tracing::error!("Failed to send packet to tunnel: {:?}", ret); + } + } + _ => { + tracing::debug!( + "Unexpected WireGuard state during decapsulation: {:?}", + decapsulate_result + ); + } + } + } + + #[tracing::instrument] + #[async_recursion] + async fn handle_routine_tun_result<'a: 'async_recursion>(&self, result: TunnResult<'a>) -> () { + match result { + TunnResult::WriteToNetwork(packet) => { + tracing::debug!( + "Sending routine packet of {} bytes to WireGuard endpoint", + packet.len() + ); + match self.udp.send_to(packet, self.endpoint).await { + Ok(_) => {} + Err(e) => { + tracing::error!( + "Failed to send routine packet to WireGuard endpoint: {:?}", + e + ); + } + }; + } + TunnResult::Err(WireGuardError::ConnectionExpired) => { + tracing::warn!("Wireguard handshake has expired!"); + + let mut buf = vec![0u8; MAX_PACKET]; + let result = self + .tunn + .lock() + .await + .format_handshake_initiation(&mut buf[..], false); + + self.handle_routine_tun_result(result).await + } + TunnResult::Err(e) => { + tracing::error!( + "Failed to prepare routine packet for WireGuard endpoint: {:?}", + e + ); + } + TunnResult::Done => { + // Sleep for a bit + tokio::time::sleep(Duration::from_millis(250)).await; + } + other => { + tracing::warn!("Unexpected WireGuard routine task state: {:?}", other); + tokio::time::sleep(Duration::from_millis(250)).await; + } + }; + } + + /// WireGuard Routine task. Handles Handshake, keep-alive, etc. + pub async fn routine_task(self) { + loop { + let mut send_buf = vec![0u8; MAX_PACKET]; + let tun_result = { self.tunn.lock().await.update_timers(&mut send_buf) }; + self.handle_routine_tun_result(tun_result).await; + } + } + + fn fill_ip_header(zc_packet: &mut ZCPacket) { + let len = zc_packet.payload_len() + PEER_MANAGER_HEADER_SIZE; + let ip_header = &mut zc_packet.mut_wg_tunnel_header().unwrap().ipv4_header; + ip_header[0] = 0x45; + ip_header[1] = 0; + ip_header[2..4].copy_from_slice(&((len + 20) as u16).to_be_bytes()); + ip_header[4..6].copy_from_slice(&0u16.to_be_bytes()); + ip_header[6..8].copy_from_slice(&0u16.to_be_bytes()); + ip_header[8] = 64; + ip_header[9] = 0; + ip_header[10..12].copy_from_slice(&0u16.to_be_bytes()); + ip_header[12..16].copy_from_slice(&0u32.to_be_bytes()); + ip_header[16..20].copy_from_slice(&0u32.to_be_bytes()); + } + + fn remove_ip_header<'a>(&self, packet: &'a [u8], is_v4: bool) -> &'a [u8] { + if is_v4 { + return &packet[20..]; + } else { + return &packet[40..]; + } + } +} + +struct WgPeer { + tunn: Option>, + udp: Arc, // only for send + config: WgConfig, + endpoint: SocketAddr, + + sink: std::sync::Mutex>>>, + + data: Option, + tasks: JoinSet<()>, + + access_time: AtomicCell, +} + +impl WgPeer { + fn new(udp: Arc, config: WgConfig, endpoint: SocketAddr) -> Self { + WgPeer { + tunn: Some(Mutex::new(Tunn::new( + config.my_secret_key.clone(), + config.peer_public_key.clone(), + None, + None, + rand::thread_rng().next_u32(), + None, + ))), + + udp, + config, + endpoint, + sink: std::sync::Mutex::new(None), + + data: None, + tasks: JoinSet::new(), + + access_time: AtomicCell::new(std::time::Instant::now()), + } + } + + async fn handle_packet_from_me(mut stream: S, data: WgPeerData) { + while let Some(Ok(packet)) = stream.next().await { + let ret = data.handle_one_packet_from_me(packet).await; + if let Err(e) = ret { + tracing::error!("Failed to handle packet from me: {}", e); + } + } + data.stopped + .store(true, std::sync::atomic::Ordering::Relaxed); + } + + async fn handle_packet_from_peer(&self, packet: &[u8]) { + self.access_time.store(std::time::Instant::now()); + tracing::trace!("Received {} bytes from peer", packet.len()); + let data = self.data.as_ref().unwrap(); + // TODO: improve this + let mut sink = self.sink.lock().unwrap().take().unwrap(); + data.handle_one_packet_from_peer(&mut sink, packet).await; + self.sink.lock().unwrap().replace(sink); + } + + fn start_and_get_tunnel(&mut self) -> Box { + let (stunnel, ctunnel) = create_ring_tunnel_pair(); + + let (stream, sink) = stunnel.split(); + + let data = WgPeerData { + udp: self.udp.clone(), + endpoint: self.endpoint, + tunn: Arc::new(self.tunn.take().unwrap()), + wg_type: self.config.wg_type.clone(), + stopped: Arc::new(AtomicBool::new(false)), + }; + + self.data = Some(data.clone()); + self.sink.lock().unwrap().replace(sink); + + self.tasks + .spawn(Self::handle_packet_from_me(stream, data.clone())); + self.tasks.spawn(data.routine_task()); + + ctunnel + } + + fn stopped(&self) -> bool { + self.data + .as_ref() + .unwrap() + .stopped + .load(std::sync::atomic::Ordering::Relaxed) + } + + async fn create_handshake_init(&self) -> Vec { + let mut dst = vec![0u8; 2048]; + let handshake_init = self + .tunn + .as_ref() + .unwrap() + .lock() + .await + .format_handshake_initiation(&mut dst, false); + assert!(matches!(handshake_init, TunnResult::WriteToNetwork(_))); + let handshake_init = if let TunnResult::WriteToNetwork(sent) = handshake_init { + sent + } else { + unreachable!(); + }; + + handshake_init.into() + } + + fn udp_socket(&self) -> Arc { + self.udp.clone() + } +} + +type ConnSender = tokio::sync::mpsc::UnboundedSender>; +type ConnReceiver = tokio::sync::mpsc::UnboundedReceiver>; + +pub struct WgTunnelListener { + addr: url::Url, + config: WgConfig, + + udp: Option>, + conn_recv: ConnReceiver, + conn_send: Option, + + wg_peer_map: Arc>>, + + tasks: JoinSet<()>, +} + +impl WgTunnelListener { + pub fn new(addr: url::Url, config: WgConfig) -> Self { + let (conn_send, conn_recv) = tokio::sync::mpsc::unbounded_channel(); + WgTunnelListener { + addr, + config, + + udp: None, + conn_recv, + conn_send: Some(conn_send), + + wg_peer_map: Arc::new(DashMap::new()), + + tasks: JoinSet::new(), + } + } + + fn get_udp_socket(&self) -> Arc { + self.udp.as_ref().unwrap().clone() + } + + async fn handle_udp_incoming( + socket: Arc, + config: WgConfig, + conn_sender: ConnSender, + peer_map: Arc>>, + ) { + let mut tasks = JoinSet::new(); + + let peer_map_clone = peer_map.clone(); + tasks.spawn(async move { + loop { + peer_map_clone.retain(|_, peer| { + peer.access_time.load().elapsed().as_secs() < 61 && !peer.stopped() + }); + tokio::time::sleep(Duration::from_secs(1)).await; + } + }); + + let mut buf = vec![0u8; MAX_PACKET]; + loop { + let Ok((n, addr)) = socket.recv_from(&mut buf).await else { + tracing::error!("Failed to receive from UDP socket"); + break; + }; + + let data = &buf[..n]; + tracing::trace!(?n, ?addr, "Received bytes from peer"); + + if !peer_map.contains_key(&addr) { + tracing::info!("New peer: {}", addr); + let mut wg = WgPeer::new(socket.clone(), config.clone(), addr.clone()); + let (stream, sink) = wg.start_and_get_tunnel().split(); + let tunnel = Box::new(TunnelWrapper::new( + stream, + sink, + Some(TunnelInfo { + tunnel_type: "wg".to_owned(), + local_addr: Some( + build_url_from_socket_addr( + &socket.local_addr().unwrap().to_string(), + "wg", + ) + .into(), + ), + remote_addr: Some( + build_url_from_socket_addr(&addr.to_string(), "wg").into(), + ), + }), + )); + if let Err(e) = conn_sender.send(tunnel) { + tracing::error!("Failed to send tunnel to conn_sender: {}", e); + } + peer_map.insert(addr, Arc::new(wg)); + } + + let peer = peer_map.get(&addr).unwrap().clone(); + peer.handle_packet_from_peer(data).await; + } + } +} + +#[async_trait] +impl TunnelListener for WgTunnelListener { + async fn listen(&mut self) -> Result<(), super::TunnelError> { + let addr = check_scheme_and_get_socket_addr::(&self.addr, "wg")?; + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(addr), + socket2::Type::DGRAM, + Some(socket2::Protocol::UDP), + )?; + + let tunnel_url: TunnelUrl = self.addr.clone().into(); + if let Some(bind_dev) = tunnel_url.bind_dev() { + setup_sokcet2_ext(&socket2_socket, &addr, Some(bind_dev))?; + } else { + setup_sokcet2(&socket2_socket, &addr)?; + } + + self.udp = Some(Arc::new(UdpSocket::from_std(socket2_socket.into())?)); + self.addr + .set_port(Some(self.udp.as_ref().unwrap().local_addr()?.port())) + .unwrap(); + + self.tasks.spawn(Self::handle_udp_incoming( + self.get_udp_socket(), + self.config.clone(), + self.conn_send.take().unwrap(), + self.wg_peer_map.clone(), + )); + + Ok(()) + } + + async fn accept(&mut self) -> Result, super::TunnelError> { + while let Some(tunnel) = self.conn_recv.recv().await { + tracing::info!(?tunnel, "Accepted tunnel"); + return Ok(tunnel); + } + Err(TunnelError::Shutdown) + } + + fn local_url(&self) -> url::Url { + self.addr.clone() + } +} + +#[derive(Clone)] +pub struct WgTunnelConnector { + addr: url::Url, + config: WgConfig, + udp: Option>, + + bind_addrs: Vec, + ip_version: IpVersion, +} + +impl Debug for WgTunnelConnector { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WgTunnelConnector") + .field("addr", &self.addr) + .field("udp", &self.udp) + .finish() + } +} + +impl WgTunnelConnector { + pub fn new(addr: url::Url, config: WgConfig) -> Self { + WgTunnelConnector { + addr, + config, + udp: None, + bind_addrs: vec![], + ip_version: IpVersion::Both, + } + } + + #[tracing::instrument(skip(config))] + async fn connect_with_socket( + addr_url: url::Url, + config: WgConfig, + udp: UdpSocket, + addr: SocketAddr, + ) -> Result, super::TunnelError> { + tracing::warn!("wg connect: {:?}", addr); + let local_addr = udp.local_addr().unwrap().to_string(); + + let mut wg_peer = WgPeer::new(Arc::new(udp), config.clone(), addr); + let udp = wg_peer.udp_socket(); + + // do handshake here so we will return after receive first packet + let handshake = wg_peer.create_handshake_init().await; + udp.send_to(&handshake, addr).await?; + let mut buf = [0u8; MAX_PACKET]; + let (n, recv_addr) = match udp.recv_from(&mut buf).await { + Ok(ret) => ret, + Err(e) => { + tracing::error!("Failed to receive handshake response: {}", e); + return Err(TunnelError::IOError(e)); + } + }; + + if recv_addr != addr { + tracing::warn!(?recv_addr, "Received packet from changed address"); + } + + let tunnel = wg_peer.start_and_get_tunnel(); + let data = wg_peer.data.as_ref().unwrap().clone(); + let mut sink = wg_peer.sink.lock().unwrap().take().unwrap(); + wg_peer.tasks.spawn(async move { + data.handle_one_packet_from_peer(&mut sink, &buf[..n]).await; + loop { + let mut buf = vec![0u8; MAX_PACKET]; + let (n, _) = match udp.recv_from(&mut buf).await { + Ok(ret) => ret, + Err(e) => { + tracing::error!("Failed to receive wg packet: {}", e); + break; + } + }; + data.handle_one_packet_from_peer(&mut sink, &buf[..n]).await; + } + }); + + let (stream, sink) = tunnel.split(); + let ret = Box::new(TunnelWrapper::new_with_associate_data( + stream, + sink, + Some(TunnelInfo { + tunnel_type: "wg".to_owned(), + local_addr: Some(super::build_url_from_socket_addr(&local_addr, "wg").into()), + remote_addr: Some(addr_url.into()), + }), + Some(Box::new(wg_peer)), + )); + + Ok(ret) + } + + async fn connect_with_ipv6( + &mut self, + addr: SocketAddr, + ) -> Result, TunnelError> { + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(addr), + socket2::Type::DGRAM, + Some(socket2::Protocol::UDP), + )?; + setup_sokcet2_ext(&socket2_socket, &"[::]:0".parse().unwrap(), None)?; + let socket = UdpSocket::from_std(socket2_socket.into())?; + Self::connect_with_socket(self.addr.clone(), self.config.clone(), socket, addr).await + } +} + +#[async_trait] +impl super::TunnelConnector for WgTunnelConnector { + #[tracing::instrument] + async fn connect(&mut self) -> Result, super::TunnelError> { + let addr = super::check_scheme_and_get_socket_addr_ext::( + &self.addr, + "wg", + self.ip_version, + )?; + + if addr.is_ipv6() { + return self.connect_with_ipv6(addr).await; + } + + let bind_addrs = if self.bind_addrs.is_empty() { + vec!["0.0.0.0:0".parse().unwrap()] + } else { + self.bind_addrs.clone() + }; + let futures = FuturesUnordered::new(); + for bind_addr in bind_addrs.into_iter() { + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(bind_addr), + socket2::Type::DGRAM, + Some(socket2::Protocol::UDP), + )?; + if let Err(e) = setup_sokcet2(&socket2_socket, &bind_addr) { + tracing::error!(bind_addr = ?bind_addr, ?addr, "bind addr fail: {:?}", e); + continue; + } + let socket = match UdpSocket::from_std(socket2_socket.into()) { + Ok(s) => s, + Err(e) => { + tracing::error!(bind_addr = ?bind_addr, ?addr, "create udp socket fail: {:?}", e); + continue; + } + }; + tracing::info!(?bind_addr, ?self.addr, "prepare wg connect task"); + futures.push(Self::connect_with_socket( + self.addr.clone(), + self.config.clone(), + socket, + addr, + )); + } + + wait_for_connect_futures(futures).await + } + + fn remote_url(&self) -> url::Url { + self.addr.clone() + } + + fn set_bind_addrs(&mut self, addrs: Vec) { + self.bind_addrs = addrs; + } + + fn set_ip_version(&mut self, ip_version: IpVersion) { + self.ip_version = ip_version; + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::tunnel::{ + common::tests::{_tunnel_bench, _tunnel_pingpong}, + TunnelConnector, + }; + use boringtun::*; + + pub fn create_wg_config() -> (WgConfig, WgConfig) { + let my_secret_key = x25519::StaticSecret::random_from_rng(rand::thread_rng()); + let my_public_key = x25519::PublicKey::from(&my_secret_key); + + let their_secret_key = x25519::StaticSecret::random_from_rng(rand::thread_rng()); + let their_public_key = x25519::PublicKey::from(&their_secret_key); + + let server_cfg = WgConfig { + my_secret_key: my_secret_key.clone(), + my_public_key, + peer_secret_key: their_secret_key.clone(), + peer_public_key: their_public_key.clone(), + wg_type: WgType::InternalUse, + }; + + let client_cfg = WgConfig { + my_secret_key: their_secret_key, + my_public_key: their_public_key, + peer_secret_key: my_secret_key, + peer_public_key: my_public_key, + wg_type: WgType::InternalUse, + }; + + (server_cfg, client_cfg) + } + + #[tokio::test] + async fn wg_pingpong() { + let (server_cfg, client_cfg) = create_wg_config(); + let listener = WgTunnelListener::new("wg://0.0.0.0:5599".parse().unwrap(), server_cfg); + let connector = WgTunnelConnector::new("wg://127.0.0.1:5599".parse().unwrap(), client_cfg); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn wg_bench() { + let (server_cfg, client_cfg) = create_wg_config(); + let listener = WgTunnelListener::new("wg://0.0.0.0:5598".parse().unwrap(), server_cfg); + let connector = WgTunnelConnector::new("wg://127.0.0.1:5598".parse().unwrap(), client_cfg); + _tunnel_bench(listener, connector).await + } + + #[tokio::test] + async fn wg_bench_with_bind() { + let (server_cfg, client_cfg) = create_wg_config(); + let listener = WgTunnelListener::new("wg://127.0.0.1:5597".parse().unwrap(), server_cfg); + let mut connector = + WgTunnelConnector::new("wg://127.0.0.1:5597".parse().unwrap(), client_cfg); + connector.set_bind_addrs(vec!["127.0.0.1:0".parse().unwrap()]); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + #[should_panic] + async fn wg_bench_with_bind_fail() { + let (server_cfg, client_cfg) = create_wg_config(); + let listener = WgTunnelListener::new("wg://127.0.0.1:5596".parse().unwrap(), server_cfg); + let mut connector = + WgTunnelConnector::new("wg://127.0.0.1:5596".parse().unwrap(), client_cfg); + connector.set_bind_addrs(vec!["10.0.0.1:0".parse().unwrap()]); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn wg_server_erase_from_map_after_close() { + let (server_cfg, client_cfg) = create_wg_config(); + let mut listener = + WgTunnelListener::new("wg://127.0.0.1:5595".parse().unwrap(), server_cfg); + listener.listen().await.unwrap(); + + const CONN_COUNT: usize = 10; + + tokio::spawn(async move { + let mut tunnels = vec![]; + for _ in 0..CONN_COUNT { + let mut connector = WgTunnelConnector::new( + "wg://127.0.0.1:5595".parse().unwrap(), + client_cfg.clone(), + ); + let ret = connector.connect().await; + assert!(ret.is_ok()); + let t = ret.unwrap(); + let (_stream, mut sink) = t.split(); + sink.send(ZCPacket::new_with_payload("payload".as_bytes())) + .await + .unwrap(); + tunnels.push(t); + } + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + }); + + for _ in 0..CONN_COUNT { + println!("accepting"); + let conn = listener.accept().await; + let (mut stream, _sink) = conn.unwrap().split(); + let packet = stream.next().await.unwrap().unwrap(); + assert_eq!("payload".as_bytes(), packet.payload()); + println!("accepting drop"); + } + + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + + assert_eq!(0, listener.wg_peer_map.len()); + } + + #[tokio::test] + async fn bind_same_port() { + let (server_cfg, _client_cfg) = create_wg_config(); + let mut listener = WgTunnelListener::new("wg://[::1]:31015".parse().unwrap(), server_cfg); + let (server_cfg, _client_cfg) = create_wg_config(); + let mut listener2 = WgTunnelListener::new("wg://[::1]:31015".parse().unwrap(), server_cfg); + listener.listen().await.unwrap(); + listener2.listen().await.unwrap(); + } + + #[tokio::test] + async fn ipv6_pingpong() { + let (server_cfg, client_cfg) = create_wg_config(); + let listener = WgTunnelListener::new("wg://[::1]:31015".parse().unwrap(), server_cfg); + let connector = WgTunnelConnector::new("wg://[::1]:31015".parse().unwrap(), client_cfg); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn ipv6_domain_pingpong() { + let (server_cfg, client_cfg) = create_wg_config(); + let listener = WgTunnelListener::new("wg://[::1]:31016".parse().unwrap(), server_cfg); + let mut connector = + WgTunnelConnector::new("wg://test.easytier.top:31016".parse().unwrap(), client_cfg); + connector.set_ip_version(IpVersion::V6); + _tunnel_pingpong(listener, connector).await; + + let (server_cfg, client_cfg) = create_wg_config(); + let listener = WgTunnelListener::new("wg://127.0.0.1:31016".parse().unwrap(), server_cfg); + let mut connector = + WgTunnelConnector::new("wg://test.easytier.top:31016".parse().unwrap(), client_cfg); + connector.set_ip_version(IpVersion::V4); + _tunnel_pingpong(listener, connector).await; + } + + #[tokio::test] + async fn test_alloc_port() { + // v4 + let (server_cfg, _client_cfg) = create_wg_config(); + let mut listener = WgTunnelListener::new("wg://0.0.0.0:0".parse().unwrap(), server_cfg); + listener.listen().await.unwrap(); + let port = listener.local_url().port().unwrap(); + assert!(port > 0); + + // v6 + let (server_cfg, _client_cfg) = create_wg_config(); + let mut listener = WgTunnelListener::new("wg://[::]:0".parse().unwrap(), server_cfg); + listener.listen().await.unwrap(); + let port = listener.local_url().port().unwrap(); + assert!(port > 0); + } +} diff --git a/rust/easytier/src/utils.rs b/rust/easytier/src/utils.rs new file mode 100644 index 0000000..da3aaf5 --- /dev/null +++ b/rust/easytier/src/utils.rs @@ -0,0 +1,176 @@ +use anyhow::Context; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; + +use crate::common::{config::ConfigLoader, get_logger_timer_rfc3339}; + +pub type PeerRoutePair = crate::proto::cli::PeerRoutePair; + +pub fn cost_to_str(cost: i32) -> String { + if cost == 1 { + "p2p".to_string() + } else { + format!("relay({})", cost) + } +} + +pub fn float_to_str(f: f64, precision: usize) -> String { + format!("{:.1$}", f, precision) +} + +pub type NewFilterSender = std::sync::mpsc::Sender; + +pub fn init_logger( + config: impl ConfigLoader, + need_reload: bool, +) -> Result, anyhow::Error> { + let file_config = config.get_file_logger_config(); + let file_level = file_config + .level + .map(|s| s.parse().unwrap()) + .unwrap_or(LevelFilter::OFF); + + let mut ret_sender: Option = None; + + // logger to rolling file + let mut file_layer = None; + if file_level != LevelFilter::OFF || need_reload { + let mut l = tracing_subscriber::fmt::layer(); + l.set_ansi(false); + let file_filter = EnvFilter::builder() + .with_default_directive(file_level.into()) + .from_env() + .with_context(|| "failed to create file filter")?; + let (file_filter, file_filter_reloader) = + tracing_subscriber::reload::Layer::new(file_filter); + + if need_reload { + let (sender, recver) = std::sync::mpsc::channel(); + ret_sender = Some(sender); + std::thread::spawn(move || { + println!("Start log filter reloader"); + while let Ok(lf) = recver.recv() { + let e = file_filter_reloader.modify(|f| { + if let Ok(nf) = EnvFilter::builder() + .with_default_directive(lf.parse::().unwrap().into()) + .from_env() + .with_context(|| "failed to create file filter") + { + println!("Reload log filter succeed, new filter level: {:?}", lf); + *f = nf; + } + }); + if e.is_err() { + println!("Failed to reload log filter: {:?}", e); + } + } + println!("Stop log filter reloader"); + }); + } + + let file_appender = tracing_appender::rolling::Builder::new() + .rotation(tracing_appender::rolling::Rotation::DAILY) + .max_log_files(5) + .filename_prefix(file_config.file.unwrap_or("easytier".to_string())) + .filename_suffix("log") + .build(file_config.dir.unwrap_or("./".to_string())) + .with_context(|| "failed to initialize rolling file appender")?; + file_layer = Some( + l.with_writer(file_appender) + .with_timer(get_logger_timer_rfc3339()) + .with_filter(file_filter), + ); + } + + // logger to console + let console_config = config.get_console_logger_config(); + let console_level = console_config + .level + .map(|s| s.parse().unwrap()) + .unwrap_or(LevelFilter::OFF); + + let console_filter = EnvFilter::builder() + .with_default_directive(console_level.into()) + .from_env() + .unwrap(); + + let console_layer = tracing_subscriber::fmt::layer() + .pretty() + .with_timer(get_logger_timer_rfc3339()) + .with_writer(std::io::stderr) + .with_filter(console_filter); + + tracing_subscriber::Registry::default() + .with(console_layer) + .with(file_layer) + .init(); + + Ok(ret_sender) +} + +#[cfg(target_os = "windows")] +pub fn utf8_or_gbk_to_string(s: &[u8]) -> String { + use encoding::{all::GBK, DecoderTrap, Encoding}; + if let Ok(utf8_str) = String::from_utf8(s.to_vec()) { + utf8_str + } else { + // 如果解码失败,则尝试使用GBK解码 + if let Ok(gbk_str) = GBK.decode(&s, DecoderTrap::Strict) { + gbk_str + } else { + String::from_utf8_lossy(s).to_string() + } + } +} + +pub fn setup_panic_handler() { + use std::backtrace; + use std::io::Write; + std::panic::set_hook(Box::new(|info| { + let backtrace = backtrace::Backtrace::force_capture(); + let payload = info.payload(); + let payload_str: Option<&str> = if let Some(s) = payload.downcast_ref::<&str>() { + Some(s) + } else if let Some(s) = payload.downcast_ref::() { + Some(s) + } else { + None + }; + + if let Some(payload_str) = payload_str { + println!( + "panic occurred: payload:{}, location: {:?}, backtrace: {:#?}", + payload_str, + info.location(), + backtrace + ); + } else { + println!( + "panic occurred: location: {:?}, backtrace: {:#?}", + info.location(), + backtrace + ); + } + println!("{}", rust_i18n::t!("core_app.panic_backtrace_save")); + let _ = std::fs::File::create("easytier-panic.log") + .and_then(|mut f| f.write_all(format!("{:?}\n{:#?}", info, backtrace).as_bytes())); + std::process::exit(1); + })); +} + +#[cfg(test)] +mod tests { + use crate::common::config::{self}; + + use super::*; + + async fn test_logger_reload() { + println!("current working dir: {:?}", std::env::current_dir()); + let config = config::TomlConfigLoader::default(); + let s = init_logger(&config, true).unwrap(); + tracing::debug!("test not display debug"); + s.unwrap().send(LevelFilter::DEBUG.to_string()).unwrap(); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + tracing::debug!("test display debug"); + } +} diff --git a/rust/easytier/src/vpn_portal/mod.rs b/rust/easytier/src/vpn_portal/mod.rs new file mode 100644 index 0000000..7a6caa8 --- /dev/null +++ b/rust/easytier/src/vpn_portal/mod.rs @@ -0,0 +1,50 @@ +// with vpn portal, user can use other vpn client to connect to easytier servers +// without installing easytier. +// these vpn client include: +// 1. wireguard +// 2. openvpn (TODO) +// 3. shadowsocks (TODO) + +use std::sync::Arc; + +use crate::{common::global_ctx::ArcGlobalCtx, peers::peer_manager::PeerManager}; + +#[cfg(feature = "wireguard")] +pub mod wireguard; + +#[async_trait::async_trait] +pub trait VpnPortal: Send + Sync { + async fn start( + &mut self, + global_ctx: ArcGlobalCtx, + peer_mgr: Arc, + ) -> anyhow::Result<()>; + async fn dump_client_config(&self, peer_mgr: Arc) -> String; + fn name(&self) -> String; + async fn list_clients(&self) -> Vec; +} + +pub struct NullVpnPortal; + +#[async_trait::async_trait] +impl VpnPortal for NullVpnPortal { + async fn start( + &mut self, + _global_ctx: ArcGlobalCtx, + _peer_mgr: Arc, + ) -> anyhow::Result<()> { + Ok(()) + } + + async fn dump_client_config(&self, _peer_mgr: Arc) -> String { + "".to_string() + } + + fn name(&self) -> String { + "null".to_string() + } + + async fn list_clients(&self) -> Vec { + vec![] + } +} diff --git a/rust/easytier/src/vpn_portal/wireguard.rs b/rust/easytier/src/vpn_portal/wireguard.rs new file mode 100644 index 0000000..ba79789 --- /dev/null +++ b/rust/easytier/src/vpn_portal/wireguard.rs @@ -0,0 +1,348 @@ +use std::{ + net::{Ipv4Addr, SocketAddr}, + sync::Arc, +}; + +use anyhow::Context; +use base64::{prelude::BASE64_STANDARD, Engine}; +use cidr::Ipv4Inet; +use dashmap::DashMap; +use futures::StreamExt; +use pnet::packet::ipv4::Ipv4Packet; +use tokio::task::JoinSet; +use tracing::Level; + +use crate::{ + common::{ + config::NetworkIdentity, + global_ctx::{ArcGlobalCtx, GlobalCtxEvent}, + join_joinset_background, + }, + peers::{peer_manager::PeerManager, PeerPacketFilter}, + tunnel::{ + mpsc::{MpscTunnel, MpscTunnelSender}, + packet_def::{PacketType, ZCPacket, ZCPacketType}, + wireguard::{WgConfig, WgTunnelListener}, + Tunnel, TunnelListener, + }, +}; + +use super::VpnPortal; + +type WgPeerIpTable = Arc>>; + +pub(crate) fn get_wg_config_for_portal(nid: &NetworkIdentity) -> WgConfig { + let key_seed = format!( + "{}{}", + nid.network_name, + nid.network_secret.as_ref().unwrap_or(&"".to_string()) + ); + WgConfig::new_for_portal(&key_seed, &key_seed) +} + +struct ClientEntry { + endpoint_addr: Option, + sink: MpscTunnelSender, +} + +struct WireGuardImpl { + global_ctx: ArcGlobalCtx, + peer_mgr: Arc, + wg_config: WgConfig, + listenr_addr: SocketAddr, + + wg_peer_ip_table: WgPeerIpTable, + + tasks: Arc>>, +} + +impl WireGuardImpl { + fn new(global_ctx: ArcGlobalCtx, peer_mgr: Arc) -> Self { + let nid = global_ctx.get_network_identity(); + let wg_config = get_wg_config_for_portal(&nid); + + let vpn_cfg = global_ctx.config.get_vpn_portal_config().unwrap(); + let listenr_addr = vpn_cfg.wireguard_listen; + + Self { + global_ctx, + peer_mgr, + wg_config, + listenr_addr, + wg_peer_ip_table: Arc::new(DashMap::new()), + tasks: Arc::new(std::sync::Mutex::new(JoinSet::new())), + } + } + + async fn handle_incoming_conn( + t: Box, + peer_mgr: Arc, + wg_peer_ip_table: WgPeerIpTable, + ) { + let info = t.info().unwrap_or_default(); + let mut mpsc_tunnel = MpscTunnel::new(t, None); + let mut stream = mpsc_tunnel.get_stream(); + let mut ip_registered = false; + + let remote_addr = info.remote_addr.clone(); + peer_mgr + .get_global_ctx() + .issue_event(GlobalCtxEvent::VpnPortalClientConnected( + info.local_addr.clone().unwrap_or_default().to_string(), + info.remote_addr.clone().unwrap_or_default().to_string(), + )); + + let mut map_key = None; + + loop { + let msg = match stream.next().await { + Some(Ok(msg)) => msg, + Some(Err(err)) => { + tracing::error!(?err, "Failed to receive from wg client"); + break; + } + None => { + tracing::info!("Wireguard client disconnected"); + break; + } + }; + + assert_eq!(msg.packet_type(), ZCPacketType::WG); + let inner = msg.inner(); + let Some(i) = Ipv4Packet::new(&inner) else { + tracing::error!(?inner, "Failed to parse ipv4 packet"); + continue; + }; + if !ip_registered { + let client_entry = Arc::new(ClientEntry { + endpoint_addr: remote_addr.clone().map(Into::into), + sink: mpsc_tunnel.get_sink(), + }); + map_key = Some(i.get_source()); + wg_peer_ip_table.insert(i.get_source(), client_entry.clone()); + ip_registered = true; + } + tracing::trace!(?i, "Received from wg client"); + let dst = i.get_destination(); + let _ = peer_mgr + .send_msg_ipv4(ZCPacket::new_with_payload(inner.as_ref()), dst) + .await; + } + + if map_key.is_some() { + tracing::info!(?map_key, "Removing wg client from table"); + wg_peer_ip_table.remove(&map_key.unwrap()); + } + + peer_mgr + .get_global_ctx() + .issue_event(GlobalCtxEvent::VpnPortalClientDisconnected( + info.local_addr.unwrap_or_default().to_string(), + info.remote_addr.unwrap_or_default().to_string(), + )); + } + + async fn start_pipeline_processor(&self) { + struct PeerPacketFilterForVpnPortal { + wg_peer_ip_table: WgPeerIpTable, + } + + #[async_trait::async_trait] + impl PeerPacketFilter for PeerPacketFilterForVpnPortal { + async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option { + let hdr = packet.peer_manager_header().unwrap(); + if hdr.packet_type != PacketType::Data as u8 { + return Some(packet); + }; + + let payload_bytes = packet.payload(); + let ipv4 = Ipv4Packet::new(payload_bytes)?; + if ipv4.get_version() != 4 { + return Some(packet); + } + + let Some(entry) = self + .wg_peer_ip_table + .get(&ipv4.get_destination()) + .map(|f| f.clone()) + else { + return Some(packet); + }; + + tracing::trace!(?ipv4, "Packet filter for vpn portal"); + + let payload_offset = packet.packet_type().get_packet_offsets().payload_offset; + let packet = ZCPacket::new_from_buf( + packet.inner().split_off(payload_offset), + ZCPacketType::WG, + ); + + match entry.sink.try_send(packet) { + Ok(_) => { + tracing::trace!("Sent packet to wg client"); + } + Err(e) => { + tracing::debug!(?e, "Failed to send packet to wg client"); + } + } + + None + } + } + + self.peer_mgr + .add_packet_process_pipeline(Box::new(PeerPacketFilterForVpnPortal { + wg_peer_ip_table: self.wg_peer_ip_table.clone(), + })) + .await; + } + + #[tracing::instrument(skip(self), err(level = Level::WARN))] + async fn start(&self) -> anyhow::Result<()> { + let mut l = WgTunnelListener::new( + format!("wg://{}", self.listenr_addr).parse().unwrap(), + self.wg_config.clone(), + ); + + tracing::info!("Wireguard VPN Portal Starting"); + + { + let _g = self.global_ctx.net_ns.guard(); + l.listen() + .await + .with_context(|| "Failed to start wireguard listener for vpn portal")?; + } + + join_joinset_background(self.tasks.clone(), "wireguard".to_string()); + + let tasks = Arc::downgrade(&self.tasks.clone()); + let peer_mgr = self.peer_mgr.clone(); + let wg_peer_ip_table = self.wg_peer_ip_table.clone(); + self.tasks.lock().unwrap().spawn(async move { + while let Ok(t) = l.accept().await { + let Some(tasks) = tasks.upgrade() else { + break; + }; + tasks.lock().unwrap().spawn(Self::handle_incoming_conn( + t, + peer_mgr.clone(), + wg_peer_ip_table.clone(), + )); + } + }); + + self.start_pipeline_processor().await; + + Ok(()) + } +} + +#[derive(Default)] +pub struct WireGuard { + inner: Option, +} + +#[async_trait::async_trait] +impl VpnPortal for WireGuard { + async fn start( + &mut self, + global_ctx: ArcGlobalCtx, + peer_mgr: Arc, + ) -> anyhow::Result<()> { + assert!(self.inner.is_none()); + + let vpn_cfg = global_ctx.config.get_vpn_portal_config(); + if vpn_cfg.is_none() { + anyhow::bail!("vpn cfg is not set for wireguard vpn portal"); + } + + let inner = WireGuardImpl::new(global_ctx, peer_mgr); + inner.start().await?; + self.inner = Some(inner); + Ok(()) + } + + async fn dump_client_config(&self, peer_mgr: Arc) -> String { + if self.inner.is_none() { + return "ERROR: Wireguard VPN Portal Not Started".to_string(); + } + let global_ctx = self.inner.as_ref().unwrap().global_ctx.clone(); + if global_ctx.config.get_vpn_portal_config().is_none() { + return "ERROR: VPN Portal Config Not Set".to_string(); + } + + let routes = peer_mgr.list_routes().await; + let mut allow_ips = routes + .iter() + .map(|x| x.proxy_cidrs.iter().map(String::to_string)) + .flatten() + .collect::>(); + for ipv4 in routes + .iter() + .filter(|x| x.ipv4_addr.is_some()) + .map(|x| x.ipv4_addr.unwrap()) + .chain(global_ctx.get_ipv4().into_iter().map(Into::into)) + { + let inet = Ipv4Inet::from(ipv4); + allow_ips.push(inet.network().to_string()); + break; + } + + let vpn_cfg = global_ctx.config.get_vpn_portal_config().unwrap(); + let client_cidr = vpn_cfg.client_cidr; + + allow_ips.push(client_cidr.to_string()); + + let allow_ips = allow_ips + .into_iter() + .map(|x| x.to_string()) + .collect::>() + .join(","); + + let cfg = self.inner.as_ref().unwrap().wg_config.clone(); + let cfg_str = format!( + r#" +[Interface] +PrivateKey = {peer_secret_key} +Address = {address} # should assign an ip from this cidr manually + +[Peer] +PublicKey = {my_public_key} +AllowedIPs = {allow_ips} +Endpoint = {listenr_addr} # should be the public ip(or domain) of the vpn server +PersistentKeepalive = 25 +"#, + peer_secret_key = BASE64_STANDARD.encode(cfg.peer_secret_key()), + my_public_key = BASE64_STANDARD.encode(cfg.my_public_key()), + listenr_addr = self.inner.as_ref().unwrap().listenr_addr, + allow_ips = allow_ips, + address = client_cidr.first_address().to_string() + "/32", + ); + + cfg_str + } + + fn name(&self) -> String { + "wireguard".to_string() + } + + async fn list_clients(&self) -> Vec { + self.inner + .as_ref() + .and_then(|w| { + Some( + w.wg_peer_ip_table + .iter() + .map(|x| { + x.value() + .endpoint_addr + .as_ref() + .map(|x| x.to_string()) + .unwrap_or_default() + }) + .collect(), + ) + }) + .unwrap_or_default() + } +} diff --git a/rust/easytier/src/web_client/controller.rs b/rust/easytier/src/web_client/controller.rs new file mode 100644 index 0000000..0d87d45 --- /dev/null +++ b/rust/easytier/src/web_client/controller.rs @@ -0,0 +1,180 @@ +use std::collections::BTreeMap; + +use dashmap::DashMap; + +use crate::{ + common::config::{ConfigLoader, TomlConfigLoader}, + launcher::NetworkInstance, + proto::{ + rpc_types::{self, controller::BaseController}, + web::{ + CollectNetworkInfoRequest, CollectNetworkInfoResponse, DeleteNetworkInstanceRequest, + DeleteNetworkInstanceResponse, ListNetworkInstanceRequest, ListNetworkInstanceResponse, + NetworkInstanceRunningInfoMap, RetainNetworkInstanceRequest, + RetainNetworkInstanceResponse, RunNetworkInstanceRequest, RunNetworkInstanceResponse, + ValidateConfigRequest, ValidateConfigResponse, WebClientService, + }, + }, +}; + +pub struct Controller { + token: String, + instance_map: DashMap, +} + +impl Controller { + pub fn new(token: String) -> Self { + Controller { + token, + instance_map: DashMap::new(), + } + } + + pub fn run_network_instance(&self, cfg: TomlConfigLoader) -> Result<(), anyhow::Error> { + let instance_id = cfg.get_id(); + if self.instance_map.contains_key(&instance_id) { + anyhow::bail!("instance {} already exists", instance_id); + } + + let mut instance = NetworkInstance::new(cfg); + instance.start()?; + + println!("instance {} started", instance_id); + self.instance_map.insert(instance_id, instance); + Ok(()) + } + + pub fn retain_network_instance( + &self, + instance_ids: Vec, + ) -> Result { + self.instance_map.retain(|k, _| instance_ids.contains(k)); + let remain = self + .instance_map + .iter() + .map(|item| item.key().clone().into()) + .collect::>(); + println!("instance {:?} retained", remain); + Ok(RetainNetworkInstanceResponse { + remain_inst_ids: remain, + }) + } + + pub fn collect_network_infos(&self) -> Result { + let mut map = BTreeMap::new(); + for instance in self.instance_map.iter() { + if let Some(info) = instance.get_running_info() { + map.insert(instance.key().to_string(), info); + } + } + Ok(NetworkInstanceRunningInfoMap { map }) + } + + pub fn list_network_instance_ids(&self) -> Vec { + self.instance_map + .iter() + .map(|item| item.key().clone()) + .collect() + } + + pub fn token(&self) -> String { + self.token.clone() + } +} + +#[async_trait::async_trait] +impl WebClientService for Controller { + type Controller = BaseController; + + async fn validate_config( + &self, + _: BaseController, + req: ValidateConfigRequest, + ) -> Result { + let toml_config = req.config.unwrap_or_default().gen_config()?.dump(); + Ok(ValidateConfigResponse { toml_config }) + } + + async fn run_network_instance( + &self, + _: BaseController, + req: RunNetworkInstanceRequest, + ) -> Result { + if req.config.is_none() { + return Err(anyhow::anyhow!("config is required").into()); + } + let cfg = req.config.unwrap().gen_config()?; + let id = cfg.get_id(); + if let Some(inst_id) = req.inst_id { + cfg.set_id(inst_id.into()); + } + self.run_network_instance(cfg)?; + Ok(RunNetworkInstanceResponse { + inst_id: Some(id.into()), + }) + } + + async fn retain_network_instance( + &self, + _: BaseController, + req: RetainNetworkInstanceRequest, + ) -> Result { + Ok(self.retain_network_instance(req.inst_ids.into_iter().map(Into::into).collect())?) + } + + async fn collect_network_info( + &self, + _: BaseController, + req: CollectNetworkInfoRequest, + ) -> Result { + let mut ret = self.collect_network_infos()?; + let include_inst_ids = req + .inst_ids + .iter() + .cloned() + .map(|id| id.to_string()) + .collect::>(); + if !include_inst_ids.is_empty() { + let mut to_remove = Vec::new(); + for (k, _) in ret.map.iter() { + if !include_inst_ids.contains(&k) { + to_remove.push(k.clone()); + } + } + + for k in to_remove { + ret.map.remove(&k); + } + } + Ok(CollectNetworkInfoResponse { info: Some(ret) }) + } + + // rpc ListNetworkInstance(ListNetworkInstanceRequest) returns (ListNetworkInstanceResponse) {} + async fn list_network_instance( + &self, + _: BaseController, + _: ListNetworkInstanceRequest, + ) -> Result { + Ok(ListNetworkInstanceResponse { + inst_ids: self + .list_network_instance_ids() + .into_iter() + .map(Into::into) + .collect(), + }) + } + + // rpc DeleteNetworkInstance(DeleteNetworkInstanceRequest) returns (DeleteNetworkInstanceResponse) {} + async fn delete_network_instance( + &self, + _: BaseController, + req: DeleteNetworkInstanceRequest, + ) -> Result { + let mut inst_ids = self.list_network_instance_ids(); + inst_ids.retain(|id| !req.inst_ids.contains(&(id.clone().into()))); + self.retain_network_instance(inst_ids.clone())?; + Ok(DeleteNetworkInstanceResponse { + remain_inst_ids: inst_ids.into_iter().map(Into::into).collect(), + }) + } +} diff --git a/rust/easytier/src/web_client/mod.rs b/rust/easytier/src/web_client/mod.rs new file mode 100644 index 0000000..524afc1 --- /dev/null +++ b/rust/easytier/src/web_client/mod.rs @@ -0,0 +1,48 @@ +use std::sync::Arc; + +use crate::{common::scoped_task::ScopedTask, tunnel::TunnelConnector}; + +pub mod controller; +pub mod session; + +pub struct WebClient { + controller: Arc, + tasks: ScopedTask<()>, +} + +impl WebClient { + pub fn new(connector: T, token: S) -> Self { + let controller = Arc::new(controller::Controller::new(token.to_string())); + + let controller_clone = controller.clone(); + let tasks = ScopedTask::from(tokio::spawn(async move { + Self::routine(controller_clone, Box::new(connector)).await; + })); + + WebClient { controller, tasks } + } + + async fn routine( + controller: Arc, + mut connector: Box, + ) { + loop { + let conn = match connector.connect().await { + Ok(conn) => conn, + Err(e) => { + println!( + "Failed to connect to the server ({}), retrying in 5 seconds...", + e + ); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + continue; + } + }; + + println!("Successfully connected to {:?}", conn.info()); + + let mut session = session::Session::new(conn, controller.clone()); + session.wait().await; + } + } +} diff --git a/rust/easytier/src/web_client/session.rs b/rust/easytier/src/web_client/session.rs new file mode 100644 index 0000000..493a537 --- /dev/null +++ b/rust/easytier/src/web_client/session.rs @@ -0,0 +1,144 @@ +use std::sync::{Arc, Weak}; + +use tokio::{ + sync::{broadcast, Mutex}, + task::JoinSet, + time::interval, +}; + +use crate::{ + common::{constants::EASYTIER_VERSION, get_machine_id}, + proto::{ + rpc_impl::bidirect::BidirectRpcManager, + rpc_types::controller::BaseController, + web::{ + HeartbeatRequest, HeartbeatResponse, WebClientServiceServer, + WebServerServiceClientFactory, + }, + }, + tunnel::Tunnel, +}; + +use super::controller::Controller; + +#[derive(Debug, Clone)] +struct HeartbeatCtx { + notifier: Arc>, + resp: Arc>>, +} + +pub struct Session { + rpc_mgr: BidirectRpcManager, + controller: Arc, + + heartbeat_ctx: HeartbeatCtx, + + tasks: Mutex>, +} + +impl Session { + pub fn new(tunnel: Box, controller: Arc) -> Self { + let rpc_mgr = BidirectRpcManager::new(); + rpc_mgr.run_with_tunnel(tunnel); + + rpc_mgr + .rpc_server() + .registry() + .register(WebClientServiceServer::new(controller.clone()), ""); + + let mut tasks: JoinSet<()> = JoinSet::new(); + let heartbeat_ctx = + Self::heartbeat_routine(&rpc_mgr, Arc::downgrade(&controller), &mut tasks); + + Session { + rpc_mgr, + controller, + heartbeat_ctx, + tasks: Mutex::new(tasks), + } + } + + fn heartbeat_routine( + rpc_mgr: &BidirectRpcManager, + controller: Weak, + tasks: &mut JoinSet<()>, + ) -> HeartbeatCtx { + let (tx, _rx1) = broadcast::channel(2); + + let ctx = HeartbeatCtx { + notifier: Arc::new(tx), + resp: Arc::new(Mutex::new(None)), + }; + + let mid = get_machine_id(); + let inst_id = uuid::Uuid::new_v4(); + let token = controller.upgrade().unwrap().token(); + let hostname = gethostname::gethostname().to_string_lossy().to_string(); + + let ctx_clone = ctx.clone(); + let mut tick = interval(std::time::Duration::from_secs(1)); + let client = rpc_mgr + .rpc_client() + .scoped_client::>(1, 1, "".to_string()); + tasks.spawn(async move { + loop { + tick.tick().await; + + let Some(controller) = controller.upgrade() else { + break; + }; + + let req = HeartbeatRequest { + machine_id: Some(mid.into()), + inst_id: Some(inst_id.into()), + user_token: token.to_string(), + + easytier_version: EASYTIER_VERSION.to_string(), + hostname: hostname.clone(), + report_time: chrono::Local::now().to_string(), + + running_network_instances: controller + .list_network_instance_ids() + .into_iter() + .map(Into::into) + .collect(), + }; + + match client + .heartbeat(BaseController::default(), req.clone()) + .await + { + Err(e) => { + tracing::error!("heartbeat failed: {:?}", e); + break; + } + Ok(resp) => { + tracing::debug!("heartbeat response: {:?}", resp); + let _ = ctx_clone.notifier.send(resp.clone()); + ctx_clone.resp.lock().await.replace(resp); + } + } + } + }); + + ctx + } + + async fn wait_routines(&self) { + self.tasks.lock().await.join_next().await; + // if any task failed, we should abort all tasks + self.tasks.lock().await.abort_all(); + } + + pub async fn wait(&mut self) { + tokio::select! { + _ = self.rpc_mgr.wait() => {} + _ = self.wait_routines() => {} + } + } + + pub async fn wait_next_heartbeat(&self) -> Option { + let mut rx = self.heartbeat_ctx.notifier.subscribe(); + rx.recv().await.ok() + } +} diff --git a/rust/easytier/third_party/Packet.dll b/rust/easytier/third_party/Packet.dll new file mode 100644 index 0000000..7e1bf12 Binary files /dev/null and b/rust/easytier/third_party/Packet.dll differ diff --git a/rust/easytier/third_party/Packet.lib b/rust/easytier/third_party/Packet.lib new file mode 100644 index 0000000..30c1540 Binary files /dev/null and b/rust/easytier/third_party/Packet.lib differ diff --git a/rust/easytier/third_party/arm64/Packet.dll b/rust/easytier/third_party/arm64/Packet.dll new file mode 100644 index 0000000..b44bb05 Binary files /dev/null and b/rust/easytier/third_party/arm64/Packet.dll differ diff --git a/rust/easytier/third_party/arm64/Packet.lib b/rust/easytier/third_party/arm64/Packet.lib new file mode 100644 index 0000000..45ab1b0 Binary files /dev/null and b/rust/easytier/third_party/arm64/Packet.lib differ diff --git a/rust/easytier/third_party/arm64/wintun.dll b/rust/easytier/third_party/arm64/wintun.dll new file mode 100644 index 0000000..dc4e4ae Binary files /dev/null and b/rust/easytier/third_party/arm64/wintun.dll differ diff --git a/rust/easytier/third_party/wintun.dll b/rust/easytier/third_party/wintun.dll new file mode 100644 index 0000000..aee04e7 Binary files /dev/null and b/rust/easytier/third_party/wintun.dll differ diff --git a/rust/src/api/mod.rs b/rust/src/api/mod.rs new file mode 100644 index 0000000..b252f36 --- /dev/null +++ b/rust/src/api/mod.rs @@ -0,0 +1 @@ +pub mod simple; diff --git a/rust/src/api/simple.rs b/rust/src/api/simple.rs new file mode 100644 index 0000000..76054a5 --- /dev/null +++ b/rust/src/api/simple.rs @@ -0,0 +1,600 @@ +pub use std::collections::BTreeMap; +//use BTreeMap +use dashmap::DashMap; +pub use easytier::common::global_ctx::{EventBusSubscriber, GlobalCtxEvent}; +use easytier::common::scoped_task::ScopedTask; +pub use easytier::{ + common::config::{ConfigLoader, TomlConfigLoader}, + launcher::NetworkInstance, + proto, +}; +pub use easytier::{ + common::{ + self, + config::{NetworkIdentity, PeerConfig}, + }, + proto::{ + cli::{ + list_peer_route_pair, ConnectorManageRpc, ConnectorManageRpcClientFactory, + DumpRouteRequest, GetVpnPortalInfoRequest, ListConnectorRequest, + ListForeignNetworkRequest, ListGlobalForeignNetworkRequest, ListPeerRequest, + ListPeerResponse, ListRouteRequest, ListRouteResponse, NodeInfo, PeerInfo, + PeerManageRpc, PeerManageRpcClientFactory, PeerRoutePair, Route, ShowNodeInfoRequest, + TcpProxyEntryState, TcpProxyEntryTransportType, TcpProxyRpc, TcpProxyRpcClientFactory, + VpnPortalRpc, VpnPortalRpcClientFactory, + }, + common::NatType, + peer_rpc::{GetGlobalPeerMapRequest, PeerCenterRpc, PeerCenterRpcClientFactory}, + rpc_impl::standalone::StandAloneClient, + rpc_types::controller::BaseController, + web::MyNodeInfo, + }, + utils::cost_to_str, +}; +use flutter_rust_bridge::frb; +use lazy_static::lazy_static; +use once_cell::sync::Lazy; +use serde_json::json; +use tokio::runtime::Runtime; +pub use tokio::task::JoinHandle; + +// pub struct Ipv4Addr { +// pub addr: u32, +// } +// pub struct Ipv4Inet { +// pub address: Option, +// pub network_length: u32, +// } + +// pub struct StunInfo { +// pub udp_nat_type: i32, +// pub tcp_nat_type: i32, +// pub last_update_time: i64, +// pub public_ip: Vec, +// pub min_port: u32, +// pub max_port: u32, +// } +// pub struct PeerFeatureFlag { +// pub is_public_server: bool, +// pub avoid_relay_data: bool, +// pub kcp_input: bool, +// pub no_relay_kcp: bool, +// } +// pub struct Route { +// pub peer_id: u32, +// pub ipv4_addr: Option, +// pub next_hop_peer_id: u32, +// pub cost: i32, +// pub path_latency: i32, +// pub proxy_cidrs: Vec, +// pub hostname: String, +// pub stun_info: Option, +// pub inst_id: String, +// pub version: String, +// pub feature_flag: Option, +// pub next_hop_peer_id_latency_first: Option, +// pub cost_latency_first: Option, +// pub path_latency_latency_first: Option, +// } +// pub struct Url { +// pub url: String, +// } +// pub struct TunnelInfo { +// pub tunnel_type: String, +// pub local_addr: Option, +// pub remote_addr: Option, +// } + +// pub struct PeerConnStats { +// pub rx_bytes: u64, +// pub tx_bytes: u64, +// pub rx_packets: u64, +// pub tx_packets: u64, +// pub latency_us: u64, +// } +// pub struct PeerConnInfo { +// pub conn_id: String, +// pub my_peer_id: u32, +// pub peer_id: u32, +// pub features: Vec, +// pub tunnel: Option, +// pub stats: Option, +// pub loss_rate: f32, +// pub is_client: bool, +// pub network_name: String, +// } +// pub struct PeerInfo { +// pub peer_id: u32, +// pub conns: Vec, +// } + +// pub struct PeerRoutePair { +// pub route: Option, +// pub peer: Option, +// } +// pub struct Runin { +// pub dev_name: String, +// pub my_node_info: Option, +// pub events: Vec, +// pub routes: Vec, +// pub peers: Vec, +// pub peer_route_pairs: Vec, +// pub running: bool, +// pub error_msg: Option, +// } +// pub struct Ipv6Addr { +// pub part1: u32, +// pub part2: u32, +// pub part3: u32, +// pub part4: u32, +// } +// pub struct GetIpListResponse { +// pub public_ipv4: Option, +// pub interface_ipv4s: Vec, +// pub public_ipv6: Option, +// pub interface_ipv6s: Vec, +// pub listeners: Vec, +// } +// pub struct MyNodeInfo { +// pub virtual_ipv4: Option, +// pub hostname: String, +// pub version: String, +// pub ips: Option, +// pub stun_info: Option, +// pub listeners: Vec, +// pub vpn_portal_cfg: Option, +// } + +static INSTANCE_MAP: Lazy> = Lazy::new(DashMap::new); +// 创建一个 NetworkInstance 类型变量 储存当前服务器 +lazy_static! { + static ref RT: Runtime = Runtime::new().expect("创建 Tokio 运行时失败"); +} + +fn create_config() -> TomlConfigLoader { + let mut cfg = TomlConfigLoader::default(); + // 构造 PeerConfig 实例并设置 peers + + cfg.set_listeners(vec![ + "tcp://0.0.0.0:11010".to_string().parse().unwrap(), + "udp://0.0.0.0:11010".to_string().parse().unwrap(), + ]); + // cfg.set_inst_name("default".to_string()); + // cfg.set_inst_name(name); + cfg +} + +// 添加一个函数来获取对等节点和路由信息 +pub fn get_peers_and_routes() -> Result<(Vec, Vec), String> { + if let Some(instance) = INSTANCE_MAP.iter().next() { + // 获取运行信息 + if let Some(info) = instance.get_running_info() { + return Ok((info.peers, info.routes)); + } + return Err("无法获取运行信息".to_string()); + } + Err("没有运行中的网络实例".to_string()) +} + +// 如果需要获取配对后的信息,可以使用这个函数 +pub fn get_peer_route_pairs() -> Result, String> { + if let Some(instance) = INSTANCE_MAP.iter().next() { + // 获取运行信息 + if let Some(info) = instance.get_running_info() { + let mut pairs = info.peer_route_pairs; + + // 如果存在本地节点信息,添加到结果中 + if let Some(my_node_info) = &info.my_node_info { + // 获取本地节点ID + let my_peer_id = info + .peers + .iter() + .find(|p| p.conns.iter().any(|c| c.is_client == false)) + .map(|p| p.peer_id) + .unwrap_or(0); + + // 创建一个表示本地节点的Route + let my_route = proto::cli::Route { + peer_id: my_peer_id, + ipv4_addr: my_node_info.virtual_ipv4.clone(), + next_hop_peer_id: my_peer_id, // 指向自己 + cost: 0, // 到自己的成本为0 + path_latency: 0, // 到自己的延迟为0 + proxy_cidrs: vec![], + hostname: my_node_info.hostname.clone(), + stun_info: my_node_info.stun_info.clone(), + inst_id: "local".to_string(), + version: my_node_info.version.clone(), + feature_flag: None, + next_hop_peer_id_latency_first: None, + cost_latency_first: None, + path_latency_latency_first: None, + }; + + // 创建一个表示本地节点的PeerInfo,包含网络统计信息 + let my_peer_info = info.peers.iter().find(|p| p.peer_id == my_peer_id).cloned(); + + // 创建一个表示本地节点的PeerRoutePair + let my_pair = proto::cli::PeerRoutePair { + route: Some(my_route), + peer: my_peer_info, // 使用找到的PeerInfo或None + }; + + // 添加到结果中 + pairs.push(my_pair); + } + + return Ok(pairs); + } + return Err("无法获取运行信息".to_string()); + } + Err("没有运行中的网络实例".to_string()) +} + +// 获取节点信息 +pub fn get_node_info() -> Result { + if let Some(instance) = INSTANCE_MAP.iter().next() { + // 获取运行信息 + if let Some(info) = instance.get_running_info() { + if let Some(node_info) = info.my_node_info { + return Ok(node_info); + } + return Err("无法获取节点信息".to_string()); + } + return Err("无法获取运行信息".to_string()); + } + Err("没有运行中的网络实例".to_string()) +} + +async fn create_and_store_network_instance(cfg: TomlConfigLoader) -> Result<(), String> { + println!("Starting easytier with config:"); + println!("############### TOML ###############\n"); + println!("{}", cfg.dump()); + println!("-----------------------------------"); + // 在移动 cfg 之前先获取 ID + let name = cfg.get_id().to_string(); + // 创建网络实例 + let mut instance = NetworkInstance::new(cfg).set_fetch_node_info(true); + // 启动网络实例,并处理可能的错误 + instance.start().unwrap(); + println!("instance {} started", name); + // 将实例存储到 INSTANCE_MAP 中 + INSTANCE_MAP.insert(name, instance); + + Ok(()) +} + +// 返回EasyTier的版本号 +pub fn easytier_version() -> Result { + Ok(easytier::VERSION.to_string()) +} + +// 是否在运行 +pub fn is_easytier_running() -> bool { + if let Some(instance) = INSTANCE_MAP.iter().next() { + instance.is_easytier_running() + } else { + false + } +} + +// 定义节点连接统计信息结构体 +pub struct KVNodeConnectionStats { + pub conn_type: String, + pub rx_bytes: u64, + pub tx_bytes: u64, + pub rx_packets: u64, + pub tx_packets: u64, +} +// 定义节点信息结构体 +pub struct KVNodeInfo { + pub hostname: String, + pub ipv4: String, + pub latency_ms: f64, + pub connections: Vec, + pub version: String, + pub cost: i32, +} +// 定义节点网络状态结构体 +pub struct KVNetworkStatus { + pub total_nodes: usize, + pub nodes: Vec, +} + +// 获取网络状态信息 +pub fn get_network_status() -> KVNetworkStatus { + let pairs = get_peer_route_pairs().unwrap_or_default(); + let mut nodes = Vec::new(); + + for pair in pairs.iter() { + if let Some(route) = &pair.route { + let cost = route.cost; + // 使用 get_running_info 中的逻辑来获取IPv4地址 + let ipv4 = route + .ipv4_addr + .as_ref() + .and_then(|addr| addr.address.as_ref()) + .map(|a| { + format!( + "{}.{}.{}.{}", + (a.addr >> 24) & 0xFF, + (a.addr >> 16) & 0xFF, + (a.addr >> 8) & 0xFF, + a.addr & 0xFF + ) + }) + .unwrap_or_else(|| "0.0.0.0".to_string()); + + let mut node_info = KVNodeInfo { + hostname: route.hostname.clone(), + ipv4, + latency_ms: f64::from(route.path_latency.max(0)) / 1000.0, + connections: Vec::new(), + version: route.version.clone(), + cost, + }; + + // 收集连接统计信息 + if let Some(peer) = &pair.peer { + for conn in &peer.conns { + if let Some(stats) = &conn.stats { + let conn_type = if let Some(tunnel) = &conn.tunnel { + tunnel.tunnel_type.clone() + } else { + "未知".to_string() + }; + + node_info.connections.push(KVNodeConnectionStats { + conn_type, + rx_bytes: stats.rx_bytes, + tx_bytes: stats.tx_bytes, + rx_packets: stats.rx_packets, + tx_packets: stats.tx_packets, + }); + } + } + } + + nodes.push(node_info); + } + } + + KVNetworkStatus { + total_nodes: nodes.len(), + nodes, + } +} + +pub fn get_running_info() -> String { + INSTANCE_MAP + .iter() + .next() + .and_then(|instance| instance.get_running_info()) + .and_then(|info| { + // 获取并打印节点路由对信息 + serde_json::to_string(&json!({ + "dev_name": info.dev_name, + "my_node_info": info.my_node_info.as_ref().map(|node| json!({ + "virtual_ipv4": node.virtual_ipv4.as_ref().map(|addr| json!({ + "address": addr.address.as_ref().map(|a| json!({ "addr": a.addr })), + "network_length": addr.network_length + })), + "hostname": node.hostname, + "version": node.version, + "ips": node.ips.as_ref().map(|ips| json!({ + "public_ipv4": ips.public_ipv4.as_ref().map(|a| json!({ "addr": a.addr })), + "interface_ipv4s": ips.interface_ipv4s.iter().map(|a| json!({ "addr": a.addr })).collect::>(), + "public_ipv6": ips.public_ipv6.as_ref().map(|a| json!({ + "part1": a.part1, + "part2": a.part2, + "part3": a.part3, + "part4": a.part4 + })), + "interface_ipv6s": ips.interface_ipv6s.iter().map(|a| json!({ + "part1": a.part1, + "part2": a.part2, + "part3": a.part3, + "part4": a.part4 + })).collect::>(), + "listeners": ips.listeners.iter().map(|l| json!({ "url": l.to_string() })).collect::>() + })), + "stun_info": node.stun_info.as_ref().map(|info| json!({ + "udp_nat_type": info.udp_nat_type, + "tcp_nat_type": info.tcp_nat_type, + "last_update_time": info.last_update_time, + "public_ip": info.public_ip, + "min_port": info.min_port, + "max_port": info.max_port + })), + "listeners": node.listeners.iter().map(|l| json!({ "url": l.url })).collect::>(), + "vpn_portal_cfg": node.vpn_portal_cfg + })), + "events": info.events, + "routes": info.routes.iter().map(|route| json!({ + "peer_id": route.peer_id, + "ipv4_addr": route.ipv4_addr.as_ref().map(|addr| json!({ + "address": addr.address.as_ref().map(|a| json!({ "addr": a.addr })), + "network_length": addr.network_length + })), + "next_hop_peer_id": route.next_hop_peer_id, + "cost": route.cost, + "path_latency": route.path_latency, + "proxy_cidrs": route.proxy_cidrs, + "hostname": route.hostname, + "stun_info": route.stun_info.as_ref().map(|info| json!({ + "udp_nat_type": info.udp_nat_type, + "tcp_nat_type": info.tcp_nat_type, + "last_update_time": info.last_update_time, + "public_ip": info.public_ip, + "min_port": info.min_port, + "max_port": info.max_port + })), + "inst_id": route.inst_id, + "version": route.version, + "feature_flag": route.feature_flag.as_ref().map(|flag| json!({ + "is_public_server": flag.is_public_server, + "avoid_relay_data": flag.avoid_relay_data, + "kcp_input": flag.kcp_input, + "no_relay_kcp": flag.no_relay_kcp + })), + "next_hop_peer_id_latency_first": route.next_hop_peer_id_latency_first, + "cost_latency_first": route.cost_latency_first, + "path_latency_latency_first": route.path_latency_latency_first + })).collect::>(), + "peers": info.peers.iter().map(|peer| json!({ + "peer_id": peer.peer_id, + "conns": peer.conns.iter().map(|conn| json!({ + "conn_id": conn.conn_id, + "my_peer_id": conn.my_peer_id, + "peer_id": conn.peer_id, + "features": conn.features, + "tunnel": conn.tunnel.as_ref().map(|t| json!({ + "tunnel_type": t.tunnel_type, + "local_addr": t.local_addr.as_ref().map(|a| json!({ "url": a.url })), + "remote_addr": t.remote_addr.as_ref().map(|a| json!({ "url": a.url })) + })), + "stats": conn.stats.as_ref().map(|s| json!({ + "rx_bytes": s.rx_bytes, + "tx_bytes": s.tx_bytes, + "rx_packets": s.rx_packets, + "tx_packets": s.tx_packets, + "latency_us": s.latency_us + })), + "loss_rate": conn.loss_rate, + "is_client": conn.is_client, + "network_name": conn.network_name + })).collect::>() + })).collect::>(), + "peer_route_pairs": info.peer_route_pairs.iter().map(|pair| json!({ + "route": pair.route.as_ref().map(|route| json!({ + "peer_id": route.peer_id, + "ipv4_addr": route.ipv4_addr.as_ref().map(|addr| json!({ + "address": addr.address.as_ref().map(|a| json!({ "addr": a.addr })), + "network_length": addr.network_length + })), + "next_hop_peer_id": route.next_hop_peer_id, + "cost": route.cost, + "path_latency": route.path_latency, + "proxy_cidrs": route.proxy_cidrs, + "hostname": route.hostname, + "stun_info": route.stun_info.as_ref().map(|info| json!({ + "udp_nat_type": info.udp_nat_type, + "tcp_nat_type": info.tcp_nat_type, + "last_update_time": info.last_update_time, + "public_ip": info.public_ip, + "min_port": info.min_port, + "max_port": info.max_port + })), + "inst_id": route.inst_id, + "version": route.version, + "feature_flag": route.feature_flag.as_ref().map(|flag| json!({ + "is_public_server": flag.is_public_server, + "avoid_relay_data": flag.avoid_relay_data, + "kcp_input": flag.kcp_input, + "no_relay_kcp": flag.no_relay_kcp + })), + "next_hop_peer_id_latency_first": route.next_hop_peer_id_latency_first, + "cost_latency_first": route.cost_latency_first, + "path_latency_latency_first": route.path_latency_latency_first + })), + "peer": pair.peer.as_ref().map(|peer| json!({ + "peer_id": peer.peer_id, + "conns": peer.conns.iter().map(|conn| json!({ + "conn_id": conn.conn_id, + "my_peer_id": conn.my_peer_id, + "peer_id": conn.peer_id, + "features": conn.features, + "tunnel": conn.tunnel.as_ref().map(|t| json!({ + "tunnel_type": t.tunnel_type, + "local_addr": t.local_addr.as_ref().map(|a| json!({ "url": a.url })), + "remote_addr": t.remote_addr.as_ref().map(|a| json!({ "url": a.url })) + })), + "stats": conn.stats.as_ref().map(|s| json!({ + "rx_bytes": s.rx_bytes, + "tx_bytes": s.tx_bytes, + "rx_packets": s.rx_packets, + "tx_packets": s.tx_packets, + "latency_us": s.latency_us + })), + "loss_rate": conn.loss_rate, + "is_client": conn.is_client, + "network_name": conn.network_name + })).collect::>() + })) + })).collect::>(), + + "running": info.running, + "error_msg": info.error_msg + })).ok() + }) + .unwrap_or_else(|| "{}".to_string()) +} + +// 创建服务器 +pub fn create_server( + username: String, + enable_dhcp: bool, + specified_ip: String, + room_name: String, + room_password: String, + severurl: String, +) { + RT.spawn(async move { + // 创建一个示例配置 + let cfg = create_config(); + cfg.set_hostname(Option::from(username)); + cfg.set_dhcp(enable_dhcp); + let peer_config = PeerConfig { + uri: ("tcp://".to_string() + &severurl).parse().unwrap(), + }; + let peer_config2 = PeerConfig { + uri: ("udp://".to_string() + &severurl).parse().unwrap(), + }; + cfg.set_peers(vec![peer_config, peer_config2]); + if enable_dhcp == false { + // 使用完整路径引用 cidr 模块的 Ipv4Inet + // 解析IP地址和子网掩码 + let ip = format!("{}/24", specified_ip).parse().unwrap(); + cfg.set_ipv4(Some(ip)); + } + cfg.set_network_identity(NetworkIdentity::new( + room_name.to_string(), + room_password.to_string(), + )); + + // 并行启动网络实例 + let handle1 = tokio::spawn(async move { + if let Err(e) = create_and_store_network_instance(cfg).await { + eprintln!("创建网络实例失败: {}", e); + } + }); + + // 等待所有任务完成 + let _ = tokio::join!(handle1); + }); +} + +// 获取INSTANCE_MAP所有的服务器然后关闭 +pub fn close_all_server() { + RT.spawn(async { + println!("关闭前实例数: {}", INSTANCE_MAP.len()); // 添加关闭前日志 + let keys: Vec<_> = INSTANCE_MAP.iter().map(|e| e.key().clone()).collect(); + println!("待关闭实例键: {:?}", keys); // 增加键列表输出 + + for key in keys { + if let Some((_, mut instance)) = INSTANCE_MAP.remove(&key) { + //丢弃 instance + drop(instance); + } else { + println!("未找到实例: {}", key); // 增加错误处理 + } + } + println!("关闭后剩余实例数: {}", INSTANCE_MAP.len()); // 添加关闭后日志 + }); +} + +pub fn init_app() { + lazy_static::initialize(&RT); + // Default utilities - feel free to customize + flutter_rust_bridge::setup_default_user_utils(); +} diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs new file mode 100644 index 0000000..2e64d15 --- /dev/null +++ b/rust/src/frb_generated.rs @@ -0,0 +1,1227 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +#![allow( + non_camel_case_types, + unused, + non_snake_case, + clippy::needless_return, + clippy::redundant_closure_call, + clippy::redundant_closure, + clippy::useless_conversion, + clippy::unit_arg, + clippy::unused_unit, + clippy::double_parens, + clippy::let_and_return, + clippy::too_many_arguments, + clippy::match_single_binding, + clippy::clone_on_copy, + clippy::let_unit_value, + clippy::deref_addrof, + clippy::explicit_auto_deref, + clippy::borrow_deref_ref, + clippy::needless_borrow +)] + +// Section: imports + +use crate::api::simple::*; +use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; +use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; +use flutter_rust_bridge::{Handler, IntoIntoDart}; + +// Section: boilerplate + +flutter_rust_bridge::frb_generated_boilerplate!( + default_stream_sink_codec = SseCodec, + default_rust_opaque = RustOpaqueMoi, + default_rust_auto_opaque = RustAutoOpaqueMoi, +); +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.9.0"; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 486890655; + +// Section: executor + +flutter_rust_bridge::frb_generated_default_handler!(); + +// Section: wire_funcs + +fn wire__crate__api__simple__close_all_server_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "close_all_server", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok({ + crate::api::simple::close_all_server(); + })?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__simple__create_server_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "create_server", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_username = ::sse_decode(&mut deserializer); + let api_enable_dhcp = ::sse_decode(&mut deserializer); + let api_specified_ip = ::sse_decode(&mut deserializer); + let api_room_name = ::sse_decode(&mut deserializer); + let api_room_password = ::sse_decode(&mut deserializer); + let api_severurl = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok({ + crate::api::simple::create_server( + api_username, + api_enable_dhcp, + api_specified_ip, + api_room_name, + api_room_password, + api_severurl, + ); + })?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__simple__easytier_version_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "easytier_version", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::simple::easytier_version()?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__simple__get_network_status_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_network_status", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::api::simple::get_network_status())?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__simple__get_node_info_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_node_info", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::simple::get_node_info()?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__simple__get_peer_route_pairs_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_peer_route_pairs", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::simple::get_peer_route_pairs()?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__simple__get_peers_and_routes_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_peers_and_routes", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, String>((move || { + let output_ok = crate::api::simple::get_peers_and_routes()?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__simple__get_running_info_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_running_info", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::api::simple::get_running_info())?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__simple__init_app_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "init_app", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok({ + crate::api::simple::init_app(); + })?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__simple__is_easytier_running_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "is_easytier_running", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::api::simple::is_easytier_running())?; + Ok(output_ok) + })()) + } + }, + ) +} + +// Section: related_funcs + +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); + +// Section: dart2rust + +impl SseDecode for MyNodeInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for PeerInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for PeerRoutePair { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for Route { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode for RustOpaqueMoi> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = >::sse_decode(deserializer); + return String::from_utf8(inner).unwrap(); + } +} + +impl SseDecode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() != 0 + } +} + +impl SseDecode for f64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_f64::().unwrap() + } +} + +impl SseDecode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_i32::().unwrap() + } +} + +impl SseDecode for crate::api::simple::KVNetworkStatus { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_totalNodes = ::sse_decode(deserializer); + let mut var_nodes = >::sse_decode(deserializer); + return crate::api::simple::KVNetworkStatus { + total_nodes: var_totalNodes, + nodes: var_nodes, + }; + } +} + +impl SseDecode for crate::api::simple::KVNodeConnectionStats { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_connType = ::sse_decode(deserializer); + let mut var_rxBytes = ::sse_decode(deserializer); + let mut var_txBytes = ::sse_decode(deserializer); + let mut var_rxPackets = ::sse_decode(deserializer); + let mut var_txPackets = ::sse_decode(deserializer); + return crate::api::simple::KVNodeConnectionStats { + conn_type: var_connType, + rx_bytes: var_rxBytes, + tx_bytes: var_txBytes, + rx_packets: var_rxPackets, + tx_packets: var_txPackets, + }; + } +} + +impl SseDecode for crate::api::simple::KVNodeInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_hostname = ::sse_decode(deserializer); + let mut var_ipv4 = ::sse_decode(deserializer); + let mut var_latencyMs = ::sse_decode(deserializer); + let mut var_connections = + >::sse_decode(deserializer); + let mut var_version = ::sse_decode(deserializer); + let mut var_cost = ::sse_decode(deserializer); + return crate::api::simple::KVNodeInfo { + hostname: var_hostname, + ipv4: var_ipv4, + latency_ms: var_latencyMs, + connections: var_connections, + version: var_version, + cost: var_cost, + }; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode( + deserializer, + )); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for (Vec, Vec) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = >::sse_decode(deserializer); + let mut var_field1 = >::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for u64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u64::().unwrap() + } +} + +impl SseDecode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() + } +} + +impl SseDecode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} +} + +impl SseDecode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u64::().unwrap() as _ + } +} + +fn pde_ffi_dispatcher_primary_impl( + func_id: i32, + port: flutter_rust_bridge::for_generated::MessagePort, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + 1 => wire__crate__api__simple__close_all_server_impl(port, ptr, rust_vec_len, data_len), + 2 => wire__crate__api__simple__create_server_impl(port, ptr, rust_vec_len, data_len), + 3 => wire__crate__api__simple__easytier_version_impl(port, ptr, rust_vec_len, data_len), + 4 => wire__crate__api__simple__get_network_status_impl(port, ptr, rust_vec_len, data_len), + 5 => wire__crate__api__simple__get_node_info_impl(port, ptr, rust_vec_len, data_len), + 6 => wire__crate__api__simple__get_peer_route_pairs_impl(port, ptr, rust_vec_len, data_len), + 7 => wire__crate__api__simple__get_peers_and_routes_impl(port, ptr, rust_vec_len, data_len), + 8 => wire__crate__api__simple__get_running_info_impl(port, ptr, rust_vec_len, data_len), + 9 => wire__crate__api__simple__init_app_impl(port, ptr, rust_vec_len, data_len), + 10 => wire__crate__api__simple__is_easytier_running_impl(port, ptr, rust_vec_len, data_len), + _ => unreachable!(), + } +} + +fn pde_ffi_dispatcher_sync_impl( + func_id: i32, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + _ => unreachable!(), + } +} + +// Section: rust2dart + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for MyNodeInfo { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for PeerInfo { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for PeerRoutePair { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for Route { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::simple::KVNetworkStatus { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.total_nodes.into_into_dart().into_dart(), + self.nodes.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::simple::KVNetworkStatus +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::simple::KVNetworkStatus +{ + fn into_into_dart(self) -> crate::api::simple::KVNetworkStatus { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::simple::KVNodeConnectionStats { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.conn_type.into_into_dart().into_dart(), + self.rx_bytes.into_into_dart().into_dart(), + self.tx_bytes.into_into_dart().into_dart(), + self.rx_packets.into_into_dart().into_dart(), + self.tx_packets.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::simple::KVNodeConnectionStats +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::simple::KVNodeConnectionStats +{ + fn into_into_dart(self) -> crate::api::simple::KVNodeConnectionStats { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::simple::KVNodeInfo { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.hostname.into_into_dart().into_dart(), + self.ipv4.into_into_dart().into_dart(), + self.latency_ms.into_into_dart().into_dart(), + self.connections.into_into_dart().into_dart(), + self.version.into_into_dart().into_dart(), + self.cost.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::simple::KVNodeInfo +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::simple::KVNodeInfo +{ + fn into_into_dart(self) -> crate::api::simple::KVNodeInfo { + self + } +} + +impl SseEncode for MyNodeInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for PeerInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for PeerRoutePair { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for Route { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode( + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), + serializer, + ); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode for RustOpaqueMoi> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.into_bytes(), serializer); + } +} + +impl SseEncode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self as _).unwrap(); + } +} + +impl SseEncode for f64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_f64::(self).unwrap(); + } +} + +impl SseEncode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_i32::(self).unwrap(); + } +} + +impl SseEncode for crate::api::simple::KVNetworkStatus { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.total_nodes, serializer); + >::sse_encode(self.nodes, serializer); + } +} + +impl SseEncode for crate::api::simple::KVNodeConnectionStats { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.conn_type, serializer); + ::sse_encode(self.rx_bytes, serializer); + ::sse_encode(self.tx_bytes, serializer); + ::sse_encode(self.rx_packets, serializer); + ::sse_encode(self.tx_packets, serializer); + } +} + +impl SseEncode for crate::api::simple::KVNodeInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.hostname, serializer); + ::sse_encode(self.ipv4, serializer); + ::sse_encode(self.latency_ms, serializer); + >::sse_encode(self.connections, serializer); + ::sse_encode(self.version, serializer); + ::sse_encode(self.cost, serializer); + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for (Vec, Vec) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.0, serializer); + >::sse_encode(self.1, serializer); + } +} + +impl SseEncode for u64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u64::(self).unwrap(); + } +} + +impl SseEncode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self).unwrap(); + } +} + +impl SseEncode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} +} + +impl SseEncode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer + .cursor + .write_u64::(self as _) + .unwrap(); + } +} + +#[cfg(not(target_family = "wasm"))] +mod io { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.9.0. + + // Section: imports + + use super::*; + use crate::api::simple::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_io!(); + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_ASTRAL_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_ASTRAL_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } +} +#[cfg(not(target_family = "wasm"))] +pub use io::*; + +/// cbindgen:ignore +#[cfg(target_family = "wasm")] +mod web { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.9.0. + + // Section: imports + + use super::*; + use crate::api::simple::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::wasm_bindgen; + use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_web!(); + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } +} +#[cfg(target_family = "wasm")] +pub use web::*; diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..cbb071f --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,2 @@ +pub mod api; +mod frb_generated; diff --git a/rust_builder/.gitignore b/rust_builder/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/rust_builder/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/rust_builder/README.md b/rust_builder/README.md new file mode 100644 index 0000000..922615f --- /dev/null +++ b/rust_builder/README.md @@ -0,0 +1 @@ +Please ignore this folder, which is just glue to build Rust with Flutter. \ No newline at end of file diff --git a/rust_builder/android/.gitignore b/rust_builder/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/rust_builder/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/rust_builder/android/build.gradle b/rust_builder/android/build.gradle new file mode 100644 index 0000000..ea00708 --- /dev/null +++ b/rust_builder/android/build.gradle @@ -0,0 +1,56 @@ +// The Android Gradle Plugin builds the native code with the Android NDK. + +group 'com.flutter_rust_bridge.rust_lib_fltier' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + // The Android Gradle Plugin knows how to build native code with the NDK. + classpath 'com.android.tools.build:gradle:7.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + if (project.android.hasProperty("namespace")) { + namespace 'com.flutter_rust_bridge.rust_lib_fltier' + } + + // Bumping the plugin compileSdkVersion requires all clients of this plugin + // to bump the version in their app. + compileSdkVersion 33 + + // Use the NDK version + // declared in /android/app/build.gradle file of the Flutter project. + // Replace it with a version number if this plugin requires a specfic NDK version. + // (e.g. ndkVersion "23.1.7779620") + ndkVersion android.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion 19 + } +} + +apply from: "../cargokit/gradle/plugin.gradle" +cargokit { + manifestDir = "../../rust" + libname = "rust_lib_fltier" +} diff --git a/rust_builder/android/settings.gradle b/rust_builder/android/settings.gradle new file mode 100644 index 0000000..c886d29 --- /dev/null +++ b/rust_builder/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'rust_lib_fltier' diff --git a/rust_builder/android/src/main/AndroidManifest.xml b/rust_builder/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..27675f9 --- /dev/null +++ b/rust_builder/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/rust_builder/cargokit/.gitignore b/rust_builder/cargokit/.gitignore new file mode 100644 index 0000000..cf7bb86 --- /dev/null +++ b/rust_builder/cargokit/.gitignore @@ -0,0 +1,4 @@ +target +.dart_tool +*.iml +!pubspec.lock diff --git a/rust_builder/cargokit/LICENSE b/rust_builder/cargokit/LICENSE new file mode 100644 index 0000000..d33a5fe --- /dev/null +++ b/rust_builder/cargokit/LICENSE @@ -0,0 +1,42 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +Copyright 2022 Matej Knopp + +================================================================================ + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +================================================================================ + +APACHE LICENSE, VERSION 2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/rust_builder/cargokit/README b/rust_builder/cargokit/README new file mode 100644 index 0000000..398474d --- /dev/null +++ b/rust_builder/cargokit/README @@ -0,0 +1,11 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +Experimental repository to provide glue for seamlessly integrating cargo build +with flutter plugins and packages. + +See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/ +for a tutorial on how to use Cargokit. + +Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin. + diff --git a/rust_builder/cargokit/build_pod.sh b/rust_builder/cargokit/build_pod.sh new file mode 100644 index 0000000..ed0e0d9 --- /dev/null +++ b/rust_builder/cargokit/build_pod.sh @@ -0,0 +1,58 @@ +#!/bin/sh +set -e + +BASEDIR=$(dirname "$0") + +# Workaround for https://github.com/dart-lang/pub/issues/4010 +BASEDIR=$(cd "$BASEDIR" ; pwd -P) + +# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project +NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"` + +export PATH=${NEW_PATH%?} # remove trailing : + +env + +# Platform name (macosx, iphoneos, iphonesimulator) +export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME + +# Arctive architectures (arm64, armv7, x86_64), space separated. +export CARGOKIT_DARWIN_ARCHS=$ARCHS + +# Current build configuration (Debug, Release) +export CARGOKIT_CONFIGURATION=$CONFIGURATION + +# Path to directory containing Cargo.toml. +export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1 + +# Temporary directory for build artifacts. +export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR + +# Output directory for final artifacts. +export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME + +# Directory to store built tool artifacts. +export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool + +# Directory inside root project. Not necessarily the top level directory of root project. +export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT + +FLUTTER_EXPORT_BUILD_ENVIRONMENT=( + "$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS + "$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS +) + +for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}" +do + if [[ -f "$path" ]]; then + source "$path" + fi +done + +sh "$BASEDIR/run_build_tool.sh" build-pod "$@" + +# Make a symlink from built framework to phony file, which will be used as input to +# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate +# attribute on custom build phase) +ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony" +ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out" diff --git a/rust_builder/cargokit/build_tool/README.md b/rust_builder/cargokit/build_tool/README.md new file mode 100644 index 0000000..a878c27 --- /dev/null +++ b/rust_builder/cargokit/build_tool/README.md @@ -0,0 +1,5 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. diff --git a/rust_builder/cargokit/build_tool/analysis_options.yaml b/rust_builder/cargokit/build_tool/analysis_options.yaml new file mode 100644 index 0000000..0e16a8b --- /dev/null +++ b/rust_builder/cargokit/build_tool/analysis_options.yaml @@ -0,0 +1,34 @@ +# This is copied from Cargokit (which is the official way to use it currently) +# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +linter: + rules: + - prefer_relative_imports + - directives_ordering + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/rust_builder/cargokit/build_tool/bin/build_tool.dart b/rust_builder/cargokit/build_tool/bin/build_tool.dart new file mode 100644 index 0000000..268eb52 --- /dev/null +++ b/rust_builder/cargokit/build_tool/bin/build_tool.dart @@ -0,0 +1,8 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'package:build_tool/build_tool.dart' as build_tool; + +void main(List arguments) { + build_tool.runMain(arguments); +} diff --git a/rust_builder/cargokit/build_tool/lib/build_tool.dart b/rust_builder/cargokit/build_tool/lib/build_tool.dart new file mode 100644 index 0000000..7c1bb75 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/build_tool.dart @@ -0,0 +1,8 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'src/build_tool.dart' as build_tool; + +Future runMain(List args) async { + return build_tool.runMain(args); +} diff --git a/rust_builder/cargokit/build_tool/lib/src/android_environment.dart b/rust_builder/cargokit/build_tool/lib/src/android_environment.dart new file mode 100644 index 0000000..15fc9ee --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/android_environment.dart @@ -0,0 +1,195 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; +import 'dart:isolate'; +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as path; +import 'package:version/version.dart'; + +import 'target.dart'; +import 'util.dart'; + +class AndroidEnvironment { + AndroidEnvironment({ + required this.sdkPath, + required this.ndkVersion, + required this.minSdkVersion, + required this.targetTempDir, + required this.target, + }); + + static void clangLinkerWrapper(List args) { + final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG']; + if (clang == null) { + throw Exception( + "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var"); + } + final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET']; + if (target == null) { + throw Exception( + "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var"); + } + + runCommand(clang, [ + target, + ...args, + ]); + } + + /// Full path to Android SDK. + final String sdkPath; + + /// Full version of Android NDK. + final String ndkVersion; + + /// Minimum supported SDK version. + final int minSdkVersion; + + /// Target directory for build artifacts. + final String targetTempDir; + + /// Target being built. + final Target target; + + bool ndkIsInstalled() { + final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); + final ndkPackageXml = File(path.join(ndkPath, 'package.xml')); + return ndkPackageXml.existsSync(); + } + + void installNdk({ + required String javaHome, + }) { + final sdkManagerExtension = Platform.isWindows ? '.bat' : ''; + final sdkManager = path.join( + sdkPath, + 'cmdline-tools', + 'latest', + 'bin', + 'sdkmanager$sdkManagerExtension', + ); + + log.info('Installing NDK $ndkVersion'); + runCommand(sdkManager, [ + '--install', + 'ndk;$ndkVersion', + ], environment: { + 'JAVA_HOME': javaHome, + }); + } + + Future> buildEnvironment() async { + final hostArch = Platform.isMacOS + ? "darwin-x86_64" + : (Platform.isLinux ? "linux-x86_64" : "windows-x86_64"); + + final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); + final toolchainPath = path.join( + ndkPath, + 'toolchains', + 'llvm', + 'prebuilt', + hostArch, + 'bin', + ); + + final minSdkVersion = + math.max(target.androidMinSdkVersion!, this.minSdkVersion); + + final exe = Platform.isWindows ? '.exe' : ''; + + final arKey = 'AR_${target.rust}'; + final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe'] + .map((e) => path.join(toolchainPath, e)) + .firstWhereOrNull((element) => File(element).existsSync()); + if (arValue == null) { + throw Exception('Failed to find ar for $target in $toolchainPath'); + } + + final targetArg = '--target=${target.rust}$minSdkVersion'; + + final ccKey = 'CC_${target.rust}'; + final ccValue = path.join(toolchainPath, 'clang$exe'); + final cfFlagsKey = 'CFLAGS_${target.rust}'; + final cFlagsValue = targetArg; + + final cxxKey = 'CXX_${target.rust}'; + final cxxValue = path.join(toolchainPath, 'clang++$exe'); + final cxxFlagsKey = 'CXXFLAGS_${target.rust}'; + final cxxFlagsValue = targetArg; + + final linkerKey = + 'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase(); + + final ranlibKey = 'RANLIB_${target.rust}'; + final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe'); + + final ndkVersionParsed = Version.parse(ndkVersion); + final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS'; + final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed); + + final runRustTool = + Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh'; + + final packagePath = (await Isolate.resolvePackageUri( + Uri.parse('package:build_tool/buildtool.dart')))! + .toFilePath(); + final selfPath = path.canonicalize(path.join( + packagePath, + '..', + '..', + '..', + runRustTool, + )); + + // Make sure that run_build_tool is working properly even initially launched directly + // through dart run. + final toolTempDir = + Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir; + + return { + arKey: arValue, + ccKey: ccValue, + cfFlagsKey: cFlagsValue, + cxxKey: cxxValue, + cxxFlagsKey: cxxFlagsValue, + ranlibKey: ranlibValue, + rustFlagsKey: rustFlagsValue, + linkerKey: selfPath, + // Recognized by main() so we know when we're acting as a wrapper + '_CARGOKIT_NDK_LINK_TARGET': targetArg, + '_CARGOKIT_NDK_LINK_CLANG': ccValue, + 'CARGOKIT_TOOL_TEMP_DIR': toolTempDir, + }; + } + + // Workaround for libgcc missing in NDK23, inspired by cargo-ndk + String _libGccWorkaround(String buildDir, Version ndkVersion) { + final workaroundDir = path.join( + buildDir, + 'cargokit', + 'libgcc_workaround', + '${ndkVersion.major}', + ); + Directory(workaroundDir).createSync(recursive: true); + if (ndkVersion.major >= 23) { + File(path.join(workaroundDir, 'libgcc.a')) + .writeAsStringSync('INPUT(-lunwind)'); + } else { + // Other way around, untested, forward libgcc.a from libunwind once Rust + // gets updated for NDK23+. + File(path.join(workaroundDir, 'libunwind.a')) + .writeAsStringSync('INPUT(-lgcc)'); + } + + var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? ''; + if (rustFlags.isNotEmpty) { + rustFlags = '$rustFlags\x1f'; + } + rustFlags = '$rustFlags-L\x1f$workaroundDir'; + return rustFlags; + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart b/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart new file mode 100644 index 0000000..e608cec --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart @@ -0,0 +1,266 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:http/http.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'builder.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'rustup.dart'; +import 'target.dart'; + +class Artifact { + /// File system location of the artifact. + final String path; + + /// Actual file name that the artifact should have in destination folder. + final String finalFileName; + + AritifactType get type { + if (finalFileName.endsWith('.dll') || + finalFileName.endsWith('.dll.lib') || + finalFileName.endsWith('.pdb') || + finalFileName.endsWith('.so') || + finalFileName.endsWith('.dylib')) { + return AritifactType.dylib; + } else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) { + return AritifactType.staticlib; + } else { + throw Exception('Unknown artifact type for $finalFileName'); + } + } + + Artifact({ + required this.path, + required this.finalFileName, + }); +} + +final _log = Logger('artifacts_provider'); + +class ArtifactProvider { + ArtifactProvider({ + required this.environment, + required this.userOptions, + }); + + final BuildEnvironment environment; + final CargokitUserOptions userOptions; + + Future>> getArtifacts(List targets) async { + final result = await _getPrecompiledArtifacts(targets); + + final pendingTargets = List.of(targets); + pendingTargets.removeWhere((element) => result.containsKey(element)); + + if (pendingTargets.isEmpty) { + return result; + } + + final rustup = Rustup(); + for (final target in targets) { + final builder = RustBuilder(target: target, environment: environment); + builder.prepare(rustup); + _log.info('Building ${environment.crateInfo.packageName} for $target'); + final targetDir = await builder.build(); + // For local build accept both static and dynamic libraries. + final artifactNames = { + ...getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + aritifactType: AritifactType.dylib, + remote: false, + ), + ...getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + aritifactType: AritifactType.staticlib, + remote: false, + ) + }; + final artifacts = artifactNames + .map((artifactName) => Artifact( + path: path.join(targetDir, artifactName), + finalFileName: artifactName, + )) + .where((element) => File(element.path).existsSync()) + .toList(); + result[target] = artifacts; + } + return result; + } + + Future>> _getPrecompiledArtifacts( + List targets) async { + if (userOptions.usePrecompiledBinaries == false) { + _log.info('Precompiled binaries are disabled'); + return {}; + } + if (environment.crateOptions.precompiledBinaries == null) { + _log.fine('Precompiled binaries not enabled for this crate'); + return {}; + } + + final start = Stopwatch()..start(); + final crateHash = CrateHash.compute(environment.manifestDir, + tempStorage: environment.targetTempDir); + _log.fine( + 'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms'); + + final downloadedArtifactsDir = + path.join(environment.targetTempDir, 'precompiled', crateHash); + Directory(downloadedArtifactsDir).createSync(recursive: true); + + final res = >{}; + + for (final target in targets) { + final requiredArtifacts = getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + remote: true, + ); + final artifactsForTarget = []; + + for (final artifact in requiredArtifacts) { + final fileName = PrecompileBinaries.fileName(target, artifact); + final downloadedPath = path.join(downloadedArtifactsDir, fileName); + if (!File(downloadedPath).existsSync()) { + final signatureFileName = + PrecompileBinaries.signatureFileName(target, artifact); + await _tryDownloadArtifacts( + crateHash: crateHash, + fileName: fileName, + signatureFileName: signatureFileName, + finalPath: downloadedPath, + ); + } + if (File(downloadedPath).existsSync()) { + artifactsForTarget.add(Artifact( + path: downloadedPath, + finalFileName: artifact, + )); + } else { + break; + } + } + + // Only provide complete set of artifacts. + if (artifactsForTarget.length == requiredArtifacts.length) { + _log.fine('Found precompiled artifacts for $target'); + res[target] = artifactsForTarget; + } + } + + return res; + } + + static Future _get(Uri url, {Map? headers}) async { + int attempt = 0; + const maxAttempts = 10; + while (true) { + try { + return await get(url, headers: headers); + } on SocketException catch (e) { + // Try to detect reset by peer error and retry. + if (attempt++ < maxAttempts && + (e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) { + _log.severe( + 'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...'); + await Future.delayed(Duration(seconds: 1)); + continue; + } else { + rethrow; + } + } + } + } + + Future _tryDownloadArtifacts({ + required String crateHash, + required String fileName, + required String signatureFileName, + required String finalPath, + }) async { + final precompiledBinaries = environment.crateOptions.precompiledBinaries!; + final prefix = precompiledBinaries.uriPrefix; + final url = Uri.parse('$prefix$crateHash/$fileName'); + final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName'); + _log.fine('Downloading signature from $signatureUrl'); + final signature = await _get(signatureUrl); + if (signature.statusCode == 404) { + _log.warning( + 'Precompiled binaries not available for crate hash $crateHash ($fileName)'); + return; + } + if (signature.statusCode != 200) { + _log.severe( + 'Failed to download signature $signatureUrl: status ${signature.statusCode}'); + return; + } + _log.fine('Downloading binary from $url'); + final res = await _get(url); + if (res.statusCode != 200) { + _log.severe('Failed to download binary $url: status ${res.statusCode}'); + return; + } + if (verify( + precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) { + File(finalPath).writeAsBytesSync(res.bodyBytes); + } else { + _log.shout('Signature verification failed! Ignoring binary.'); + } + } +} + +enum AritifactType { + staticlib, + dylib, +} + +AritifactType artifactTypeForTarget(Target target) { + if (target.darwinPlatform != null) { + return AritifactType.staticlib; + } else { + return AritifactType.dylib; + } +} + +List getArtifactNames({ + required Target target, + required String libraryName, + required bool remote, + AritifactType? aritifactType, +}) { + aritifactType ??= artifactTypeForTarget(target); + if (target.darwinArch != null) { + if (aritifactType == AritifactType.staticlib) { + return ['lib$libraryName.a']; + } else { + return ['lib$libraryName.dylib']; + } + } else if (target.rust.contains('-windows-')) { + if (aritifactType == AritifactType.staticlib) { + return ['$libraryName.lib']; + } else { + return [ + '$libraryName.dll', + '$libraryName.dll.lib', + if (!remote) '$libraryName.pdb' + ]; + } + } else if (target.rust.contains('-linux-')) { + if (aritifactType == AritifactType.staticlib) { + return ['lib$libraryName.a']; + } else { + return ['lib$libraryName.so']; + } + } else { + throw Exception("Unsupported target: ${target.rust}"); + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart b/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart new file mode 100644 index 0000000..6f3b2a4 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/build_cmake.dart @@ -0,0 +1,40 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; + +class BuildCMake { + final CargokitUserOptions userOptions; + + BuildCMake({required this.userOptions}); + + Future build() async { + final targetPlatform = Environment.targetPlatform; + final target = Target.forFlutterName(Environment.targetPlatform); + if (target == null) { + throw Exception("Unknown target platform: $targetPlatform"); + } + + final environment = BuildEnvironment.fromEnvironment(isAndroid: false); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts([target]); + + final libs = artifacts[target]!; + + for (final lib in libs) { + if (lib.type == AritifactType.dylib) { + File(lib.path) + .copySync(path.join(Environment.outputDir, lib.finalFileName)); + } + } + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart b/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart new file mode 100644 index 0000000..7e61fcb --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/build_gradle.dart @@ -0,0 +1,49 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; + +final log = Logger('build_gradle'); + +class BuildGradle { + BuildGradle({required this.userOptions}); + + final CargokitUserOptions userOptions; + + Future build() async { + final targets = Environment.targetPlatforms.map((arch) { + final target = Target.forFlutterName(arch); + if (target == null) { + throw Exception( + "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); + } + return target; + }).toList(); + + final environment = BuildEnvironment.fromEnvironment(isAndroid: true); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts(targets); + + for (final target in targets) { + final libs = artifacts[target]!; + final outputDir = path.join(Environment.outputDir, target.android!); + Directory(outputDir).createSync(recursive: true); + + for (final lib in libs) { + if (lib.type == AritifactType.dylib) { + File(lib.path).copySync(path.join(outputDir, lib.finalFileName)); + } + } + } + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/build_pod.dart b/rust_builder/cargokit/build_tool/lib/src/build_pod.dart new file mode 100644 index 0000000..8a9c0db --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/build_pod.dart @@ -0,0 +1,89 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; +import 'util.dart'; + +class BuildPod { + BuildPod({required this.userOptions}); + + final CargokitUserOptions userOptions; + + Future build() async { + final targets = Environment.darwinArchs.map((arch) { + final target = Target.forDarwin( + platformName: Environment.darwinPlatformName, darwinAarch: arch); + if (target == null) { + throw Exception( + "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); + } + return target; + }).toList(); + + final environment = BuildEnvironment.fromEnvironment(isAndroid: false); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts(targets); + + void performLipo(String targetFile, Iterable sourceFiles) { + runCommand("lipo", [ + '-create', + ...sourceFiles, + '-output', + targetFile, + ]); + } + + final outputDir = Environment.outputDir; + + Directory(outputDir).createSync(recursive: true); + + final staticLibs = artifacts.values + .expand((element) => element) + .where((element) => element.type == AritifactType.staticlib) + .toList(); + final dynamicLibs = artifacts.values + .expand((element) => element) + .where((element) => element.type == AritifactType.dylib) + .toList(); + + final libName = environment.crateInfo.packageName; + + // If there is static lib, use it and link it with pod + if (staticLibs.isNotEmpty) { + final finalTargetFile = path.join(outputDir, "lib$libName.a"); + performLipo(finalTargetFile, staticLibs.map((e) => e.path)); + } else { + // Otherwise try to replace bundle dylib with our dylib + final bundlePaths = [ + '$libName.framework/Versions/A/$libName', + '$libName.framework/$libName', + ]; + + for (final bundlePath in bundlePaths) { + final targetFile = path.join(outputDir, bundlePath); + if (File(targetFile).existsSync()) { + performLipo(targetFile, dynamicLibs.map((e) => e.path)); + + // Replace absolute id with @rpath one so that it works properly + // when moved to Frameworks. + runCommand("install_name_tool", [ + '-id', + '@rpath/$bundlePath', + targetFile, + ]); + return; + } + } + throw Exception('Unable to find bundle for dynamic library'); + } + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/build_tool.dart b/rust_builder/cargokit/build_tool/lib/src/build_tool.dart new file mode 100644 index 0000000..c8f3698 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/build_tool.dart @@ -0,0 +1,271 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:github/github.dart'; +import 'package:hex/hex.dart'; +import 'package:logging/logging.dart'; + +import 'android_environment.dart'; +import 'build_cmake.dart'; +import 'build_gradle.dart'; +import 'build_pod.dart'; +import 'logging.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'target.dart'; +import 'util.dart'; +import 'verify_binaries.dart'; + +final log = Logger('build_tool'); + +abstract class BuildCommand extends Command { + Future runBuildCommand(CargokitUserOptions options); + + @override + Future run() async { + final options = CargokitUserOptions.load(); + + if (options.verboseLogging || + Platform.environment['CARGOKIT_VERBOSE'] == '1') { + enableVerboseLogging(); + } + + await runBuildCommand(options); + } +} + +class BuildPodCommand extends BuildCommand { + @override + final name = 'build-pod'; + + @override + final description = 'Build cocoa pod library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildPod(userOptions: options); + await build.build(); + } +} + +class BuildGradleCommand extends BuildCommand { + @override + final name = 'build-gradle'; + + @override + final description = 'Build android library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildGradle(userOptions: options); + await build.build(); + } +} + +class BuildCMakeCommand extends BuildCommand { + @override + final name = 'build-cmake'; + + @override + final description = 'Build CMake library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildCMake(userOptions: options); + await build.build(); + } +} + +class GenKeyCommand extends Command { + @override + final name = 'gen-key'; + + @override + final description = 'Generate key pair for signing precompiled binaries'; + + @override + void run() { + final kp = generateKey(); + final private = HEX.encode(kp.privateKey.bytes); + final public = HEX.encode(kp.publicKey.bytes); + print("Private Key: $private"); + print("Public Key: $public"); + } +} + +class PrecompileBinariesCommand extends Command { + PrecompileBinariesCommand() { + argParser + ..addOption( + 'repository', + mandatory: true, + help: 'Github repository slug in format owner/name', + ) + ..addOption( + 'manifest-dir', + mandatory: true, + help: 'Directory containing Cargo.toml', + ) + ..addMultiOption('target', + help: 'Rust target triple of artifact to build.\n' + 'Can be specified multiple times or omitted in which case\n' + 'all targets for current platform will be built.') + ..addOption( + 'android-sdk-location', + help: 'Location of Android SDK (if available)', + ) + ..addOption( + 'android-ndk-version', + help: 'Android NDK version (if available)', + ) + ..addOption( + 'android-min-sdk-version', + help: 'Android minimum rquired version (if available)', + ) + ..addOption( + 'temp-dir', + help: 'Directory to store temporary build artifacts', + ) + ..addFlag( + "verbose", + abbr: "v", + defaultsTo: false, + help: "Enable verbose logging", + ); + } + + @override + final name = 'precompile-binaries'; + + @override + final description = 'Prebuild and upload binaries\n' + 'Private key must be passed through PRIVATE_KEY environment variable. ' + 'Use gen_key through generate priave key.\n' + 'Github token must be passed as GITHUB_TOKEN environment variable.\n'; + + @override + Future run() async { + final verbose = argResults!['verbose'] as bool; + if (verbose) { + enableVerboseLogging(); + } + + final privateKeyString = Platform.environment['PRIVATE_KEY']; + if (privateKeyString == null) { + throw ArgumentError('Missing PRIVATE_KEY environment variable'); + } + final githubToken = Platform.environment['GITHUB_TOKEN']; + if (githubToken == null) { + throw ArgumentError('Missing GITHUB_TOKEN environment variable'); + } + final privateKey = HEX.decode(privateKeyString); + if (privateKey.length != 64) { + throw ArgumentError('Private key must be 64 bytes long'); + } + final manifestDir = argResults!['manifest-dir'] as String; + if (!Directory(manifestDir).existsSync()) { + throw ArgumentError('Manifest directory does not exist: $manifestDir'); + } + String? androidMinSdkVersionString = + argResults!['android-min-sdk-version'] as String?; + int? androidMinSdkVersion; + if (androidMinSdkVersionString != null) { + androidMinSdkVersion = int.tryParse(androidMinSdkVersionString); + if (androidMinSdkVersion == null) { + throw ArgumentError( + 'Invalid android-min-sdk-version: $androidMinSdkVersionString'); + } + } + final targetStrigns = argResults!['target'] as List; + final targets = targetStrigns.map((target) { + final res = Target.forRustTriple(target); + if (res == null) { + throw ArgumentError('Invalid target: $target'); + } + return res; + }).toList(growable: false); + final precompileBinaries = PrecompileBinaries( + privateKey: PrivateKey(privateKey), + githubToken: githubToken, + manifestDir: manifestDir, + repositorySlug: RepositorySlug.full(argResults!['repository'] as String), + targets: targets, + androidSdkLocation: argResults!['android-sdk-location'] as String?, + androidNdkVersion: argResults!['android-ndk-version'] as String?, + androidMinSdkVersion: androidMinSdkVersion, + tempDir: argResults!['temp-dir'] as String?, + ); + + await precompileBinaries.run(); + } +} + +class VerifyBinariesCommand extends Command { + VerifyBinariesCommand() { + argParser.addOption( + 'manifest-dir', + mandatory: true, + help: 'Directory containing Cargo.toml', + ); + } + + @override + final name = "verify-binaries"; + + @override + final description = 'Verifies published binaries\n' + 'Checks whether there is a binary published for each targets\n' + 'and checks the signature.'; + + @override + Future run() async { + final manifestDir = argResults!['manifest-dir'] as String; + final verifyBinaries = VerifyBinaries( + manifestDir: manifestDir, + ); + await verifyBinaries.run(); + } +} + +Future runMain(List args) async { + try { + // Init logging before options are loaded + initLogging(); + + if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) { + return AndroidEnvironment.clangLinkerWrapper(args); + } + + final runner = CommandRunner('build_tool', 'Cargokit built_tool') + ..addCommand(BuildPodCommand()) + ..addCommand(BuildGradleCommand()) + ..addCommand(BuildCMakeCommand()) + ..addCommand(GenKeyCommand()) + ..addCommand(PrecompileBinariesCommand()) + ..addCommand(VerifyBinariesCommand()); + + await runner.run(args); + } on ArgumentError catch (e) { + stderr.writeln(e.toString()); + exit(1); + } catch (e, s) { + log.severe(kDoubleSeparator); + log.severe('Cargokit BuildTool failed with error:'); + log.severe(kSeparator); + log.severe(e); + // This tells user to install Rust, there's no need to pollute the log with + // stack trace. + if (e is! RustupNotFoundException) { + log.severe(kSeparator); + log.severe(s); + log.severe(kSeparator); + log.severe('BuildTool arguments: $args'); + } + log.severe(kDoubleSeparator); + exit(1); + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/builder.dart b/rust_builder/cargokit/build_tool/lib/src/builder.dart new file mode 100644 index 0000000..84c46e4 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/builder.dart @@ -0,0 +1,198 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'package:collection/collection.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'android_environment.dart'; +import 'cargo.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'rustup.dart'; +import 'target.dart'; +import 'util.dart'; + +final _log = Logger('builder'); + +enum BuildConfiguration { + debug, + release, + profile, +} + +extension on BuildConfiguration { + bool get isDebug => this == BuildConfiguration.debug; + String get rustName => switch (this) { + BuildConfiguration.debug => 'debug', + BuildConfiguration.release => 'release', + BuildConfiguration.profile => 'release', + }; +} + +class BuildException implements Exception { + final String message; + + BuildException(this.message); + + @override + String toString() { + return 'BuildException: $message'; + } +} + +class BuildEnvironment { + final BuildConfiguration configuration; + final CargokitCrateOptions crateOptions; + final String targetTempDir; + final String manifestDir; + final CrateInfo crateInfo; + + final bool isAndroid; + final String? androidSdkPath; + final String? androidNdkVersion; + final int? androidMinSdkVersion; + final String? javaHome; + + BuildEnvironment({ + required this.configuration, + required this.crateOptions, + required this.targetTempDir, + required this.manifestDir, + required this.crateInfo, + required this.isAndroid, + this.androidSdkPath, + this.androidNdkVersion, + this.androidMinSdkVersion, + this.javaHome, + }); + + static BuildConfiguration parseBuildConfiguration(String value) { + // XCode configuration adds the flavor to configuration name. + final firstSegment = value.split('-').first; + final buildConfiguration = BuildConfiguration.values.firstWhereOrNull( + (e) => e.name == firstSegment, + ); + if (buildConfiguration == null) { + _log.warning('Unknown build configuraiton $value, will assume release'); + return BuildConfiguration.release; + } + return buildConfiguration; + } + + static BuildEnvironment fromEnvironment({ + required bool isAndroid, + }) { + final buildConfiguration = + parseBuildConfiguration(Environment.configuration); + final manifestDir = Environment.manifestDir; + final crateOptions = CargokitCrateOptions.load( + manifestDir: manifestDir, + ); + final crateInfo = CrateInfo.load(manifestDir); + return BuildEnvironment( + configuration: buildConfiguration, + crateOptions: crateOptions, + targetTempDir: Environment.targetTempDir, + manifestDir: manifestDir, + crateInfo: crateInfo, + isAndroid: isAndroid, + androidSdkPath: isAndroid ? Environment.sdkPath : null, + androidNdkVersion: isAndroid ? Environment.ndkVersion : null, + androidMinSdkVersion: + isAndroid ? int.parse(Environment.minSdkVersion) : null, + javaHome: isAndroid ? Environment.javaHome : null, + ); + } +} + +class RustBuilder { + final Target target; + final BuildEnvironment environment; + + RustBuilder({ + required this.target, + required this.environment, + }); + + void prepare( + Rustup rustup, + ) { + final toolchain = _toolchain; + if (rustup.installedTargets(toolchain) == null) { + rustup.installToolchain(toolchain); + } + if (toolchain == 'nightly') { + rustup.installRustSrcForNightly(); + } + if (!rustup.installedTargets(toolchain)!.contains(target.rust)) { + rustup.installTarget(target.rust, toolchain: toolchain); + } + } + + CargoBuildOptions? get _buildOptions => + environment.crateOptions.cargo[environment.configuration]; + + String get _toolchain => _buildOptions?.toolchain.name ?? 'stable'; + + /// Returns the path of directory containing build artifacts. + Future build() async { + final extraArgs = _buildOptions?.flags ?? []; + final manifestPath = path.join(environment.manifestDir, 'Cargo.toml'); + runCommand( + 'rustup', + [ + 'run', + _toolchain, + 'cargo', + 'build', + ...extraArgs, + '--manifest-path', + manifestPath, + '-p', + environment.crateInfo.packageName, + if (!environment.configuration.isDebug) '--release', + '--target', + target.rust, + '--target-dir', + environment.targetTempDir, + ], + environment: await _buildEnvironment(), + ); + return path.join( + environment.targetTempDir, + target.rust, + environment.configuration.rustName, + ); + } + + Future> _buildEnvironment() async { + if (target.android == null) { + return {}; + } else { + final sdkPath = environment.androidSdkPath; + final ndkVersion = environment.androidNdkVersion; + final minSdkVersion = environment.androidMinSdkVersion; + if (sdkPath == null) { + throw BuildException('androidSdkPath is not set'); + } + if (ndkVersion == null) { + throw BuildException('androidNdkVersion is not set'); + } + if (minSdkVersion == null) { + throw BuildException('androidMinSdkVersion is not set'); + } + final env = AndroidEnvironment( + sdkPath: sdkPath, + ndkVersion: ndkVersion, + minSdkVersion: minSdkVersion, + targetTempDir: environment.targetTempDir, + target: target, + ); + if (!env.ndkIsInstalled() && environment.javaHome != null) { + env.installNdk(javaHome: environment.javaHome!); + } + return env.buildEnvironment(); + } + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/cargo.dart b/rust_builder/cargokit/build_tool/lib/src/cargo.dart new file mode 100644 index 0000000..0d8958f --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/cargo.dart @@ -0,0 +1,48 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:toml/toml.dart'; + +class ManifestException { + ManifestException(this.message, {required this.fileName}); + + final String? fileName; + final String message; + + @override + String toString() { + if (fileName != null) { + return 'Failed to parse package manifest at $fileName: $message'; + } else { + return 'Failed to parse package manifest: $message'; + } + } +} + +class CrateInfo { + CrateInfo({required this.packageName}); + + final String packageName; + + static CrateInfo parseManifest(String manifest, {final String? fileName}) { + final toml = TomlDocument.parse(manifest); + final package = toml.toMap()['package']; + if (package == null) { + throw ManifestException('Missing package section', fileName: fileName); + } + final name = package['name']; + if (name == null) { + throw ManifestException('Missing package name', fileName: fileName); + } + return CrateInfo(packageName: name); + } + + static CrateInfo load(String manifestDir) { + final manifestFile = File(path.join(manifestDir, 'Cargo.toml')); + final manifest = manifestFile.readAsStringSync(); + return parseManifest(manifest, fileName: manifestFile.path); + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart b/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart new file mode 100644 index 0000000..0c4d88d --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/crate_hash.dart @@ -0,0 +1,124 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; +import 'package:path/path.dart' as path; + +class CrateHash { + /// Computes a hash uniquely identifying crate content. This takes into account + /// content all all .rs files inside the src directory, as well as Cargo.toml, + /// Cargo.lock, build.rs and cargokit.yaml. + /// + /// If [tempStorage] is provided, computed hash is stored in a file in that directory + /// and reused on subsequent calls if the crate content hasn't changed. + static String compute(String manifestDir, {String? tempStorage}) { + return CrateHash._( + manifestDir: manifestDir, + tempStorage: tempStorage, + )._compute(); + } + + CrateHash._({ + required this.manifestDir, + required this.tempStorage, + }); + + String _compute() { + final files = getFiles(); + final tempStorage = this.tempStorage; + if (tempStorage != null) { + final quickHash = _computeQuickHash(files); + final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash')); + quickHashFolder.createSync(recursive: true); + final quickHashFile = File(path.join(quickHashFolder.path, quickHash)); + if (quickHashFile.existsSync()) { + return quickHashFile.readAsStringSync(); + } + final hash = _computeHash(files); + quickHashFile.writeAsStringSync(hash); + return hash; + } else { + return _computeHash(files); + } + } + + /// Computes a quick hash based on files stat (without reading contents). This + /// is used to cache the real hash, which is slower to compute since it involves + /// reading every single file. + String _computeQuickHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + final data = ByteData(8); + for (final file in files) { + input.add(utf8.encode(file.path)); + final stat = file.statSync(); + data.setUint64(0, stat.size); + input.add(data.buffer.asUint8List()); + data.setUint64(0, stat.modified.millisecondsSinceEpoch); + input.add(data.buffer.asUint8List()); + } + + input.close(); + return base64Url.encode(output.events.single.bytes); + } + + String _computeHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + void addTextFile(File file) { + // text Files are hashed by lines in case we're dealing with github checkout + // that auto-converts line endings. + final splitter = LineSplitter(); + if (file.existsSync()) { + final data = file.readAsStringSync(); + final lines = splitter.convert(data); + for (final line in lines) { + input.add(utf8.encode(line)); + } + } + } + + for (final file in files) { + addTextFile(file); + } + + input.close(); + final res = output.events.single; + + // Truncate to 128bits. + final hash = res.bytes.sublist(0, 16); + return hex.encode(hash); + } + + List getFiles() { + final src = Directory(path.join(manifestDir, 'src')); + final files = src + .listSync(recursive: true, followLinks: false) + .whereType() + .toList(); + files.sortBy((element) => element.path); + void addFile(String relative) { + final file = File(path.join(manifestDir, relative)); + if (file.existsSync()) { + files.add(file); + } + } + + addFile('Cargo.toml'); + addFile('Cargo.lock'); + addFile('build.rs'); + addFile('cargokit.yaml'); + return files; + } + + final String manifestDir; + final String? tempStorage; +} diff --git a/rust_builder/cargokit/build_tool/lib/src/environment.dart b/rust_builder/cargokit/build_tool/lib/src/environment.dart new file mode 100644 index 0000000..996483a --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/environment.dart @@ -0,0 +1,68 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +extension on String { + String resolveSymlink() => File(this).resolveSymbolicLinksSync(); +} + +class Environment { + /// Current build configuration (debug or release). + static String get configuration => + _getEnv("CARGOKIT_CONFIGURATION").toLowerCase(); + + static bool get isDebug => configuration == 'debug'; + static bool get isRelease => configuration == 'release'; + + /// Temporary directory where Rust build artifacts are placed. + static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR"); + + /// Final output directory where the build artifacts are placed. + static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR'); + + /// Path to the crate manifest (containing Cargo.toml). + static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR'); + + /// Directory inside root project. Not necessarily root folder. Symlinks are + /// not resolved on purpose. + static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR'); + + // Pod + + /// Platform name (macosx, iphoneos, iphonesimulator). + static String get darwinPlatformName => + _getEnv("CARGOKIT_DARWIN_PLATFORM_NAME"); + + /// List of architectures to build for (arm64, armv7, x86_64). + static List get darwinArchs => + _getEnv("CARGOKIT_DARWIN_ARCHS").split(' '); + + // Gradle + static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION"); + static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION"); + static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR"); + static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME"); + static List get targetPlatforms => + _getEnv("CARGOKIT_TARGET_PLATFORMS").split(','); + + // CMAKE + static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM"); + + static String _getEnv(String key) { + final res = Platform.environment[key]; + if (res == null) { + throw Exception("Missing environment variable $key"); + } + return res; + } + + static String _getEnvPath(String key) { + final res = _getEnv(key); + if (Directory(res).existsSync()) { + return res.resolveSymlink(); + } else { + return res; + } + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/logging.dart b/rust_builder/cargokit/build_tool/lib/src/logging.dart new file mode 100644 index 0000000..5edd4fd --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/logging.dart @@ -0,0 +1,52 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:logging/logging.dart'; + +const String kSeparator = "--"; +const String kDoubleSeparator = "=="; + +bool _lastMessageWasSeparator = false; + +void _log(LogRecord rec) { + final prefix = '${rec.level.name}: '; + final out = rec.level == Level.SEVERE ? stderr : stdout; + if (rec.message == kSeparator) { + if (!_lastMessageWasSeparator) { + out.write(prefix); + out.writeln('-' * 80); + _lastMessageWasSeparator = true; + } + return; + } else if (rec.message == kDoubleSeparator) { + out.write(prefix); + out.writeln('=' * 80); + _lastMessageWasSeparator = true; + return; + } + out.write(prefix); + out.writeln(rec.message); + _lastMessageWasSeparator = false; +} + +void initLogging() { + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((LogRecord rec) { + final lines = rec.message.split('\n'); + for (final line in lines) { + if (line.isNotEmpty || lines.length == 1 || line != lines.last) { + _log(LogRecord( + rec.level, + line, + rec.loggerName, + )); + } + } + }); +} + +void enableVerboseLogging() { + Logger.root.level = Level.ALL; +} diff --git a/rust_builder/cargokit/build_tool/lib/src/options.dart b/rust_builder/cargokit/build_tool/lib/src/options.dart new file mode 100644 index 0000000..22aef1d --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/options.dart @@ -0,0 +1,309 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:hex/hex.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +import 'builder.dart'; +import 'environment.dart'; +import 'rustup.dart'; + +final _log = Logger('options'); + +/// A class for exceptions that have source span information attached. +class SourceSpanException implements Exception { + // This is a getter so that subclasses can override it. + /// A message describing the exception. + String get message => _message; + final String _message; + + // This is a getter so that subclasses can override it. + /// The span associated with this exception. + /// + /// This may be `null` if the source location can't be determined. + SourceSpan? get span => _span; + final SourceSpan? _span; + + SourceSpanException(this._message, this._span); + + /// Returns a string representation of `this`. + /// + /// [color] may either be a [String], a [bool], or `null`. If it's a string, + /// it indicates an ANSI terminal color escape that should be used to + /// highlight the span's text. If it's `true`, it indicates that the text + /// should be highlighted using the default color. If it's `false` or `null`, + /// it indicates that the text shouldn't be highlighted. + @override + String toString({Object? color}) { + if (span == null) return message; + return 'Error on ${span!.message(message, color: color)}'; + } +} + +enum Toolchain { + stable, + beta, + nightly, +} + +class CargoBuildOptions { + final Toolchain toolchain; + final List flags; + + CargoBuildOptions({ + required this.toolchain, + required this.flags, + }); + + static Toolchain _toolchainFromNode(YamlNode node) { + if (node case YamlScalar(value: String name)) { + final toolchain = + Toolchain.values.firstWhereOrNull((element) => element.name == name); + if (toolchain != null) { + return toolchain; + } + } + throw SourceSpanException( + 'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.', + node.span); + } + + static CargoBuildOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargo options must be a map', node.span); + } + Toolchain toolchain = Toolchain.stable; + List flags = []; + for (final MapEntry(:key, :value) in node.nodes.entries) { + if (key case YamlScalar(value: 'toolchain')) { + toolchain = _toolchainFromNode(value); + } else if (key case YamlScalar(value: 'extra_flags')) { + if (value case YamlList(nodes: List list)) { + if (list.every((element) { + if (element case YamlScalar(value: String _)) { + return true; + } + return false; + })) { + flags = list.map((e) => e.value as String).toList(); + continue; + } + } + throw SourceSpanException( + 'Extra flags must be a list of strings', value.span); + } else { + throw SourceSpanException( + 'Unknown cargo option type. Must be "toolchain" or "extra_flags".', + key.span); + } + } + return CargoBuildOptions(toolchain: toolchain, flags: flags); + } +} + +extension on YamlMap { + /// Map that extracts keys so that we can do map case check on them. + Map get valueMap => + nodes.map((key, value) => MapEntry(key.value, value)); +} + +class PrecompiledBinaries { + final String uriPrefix; + final PublicKey publicKey; + + PrecompiledBinaries({ + required this.uriPrefix, + required this.publicKey, + }); + + static PublicKey _publicKeyFromHex(String key, SourceSpan? span) { + final bytes = HEX.decode(key); + if (bytes.length != 32) { + throw SourceSpanException( + 'Invalid public key. Must be 32 bytes long.', span); + } + return PublicKey(bytes); + } + + static PrecompiledBinaries parse(YamlNode node) { + if (node case YamlMap(valueMap: Map map)) { + if (map + case { + 'url_prefix': YamlNode urlPrefixNode, + 'public_key': YamlNode publicKeyNode, + }) { + final urlPrefix = switch (urlPrefixNode) { + YamlScalar(value: String urlPrefix) => urlPrefix, + _ => throw SourceSpanException( + 'Invalid URL prefix value.', urlPrefixNode.span), + }; + final publicKey = switch (publicKeyNode) { + YamlScalar(value: String publicKey) => + _publicKeyFromHex(publicKey, publicKeyNode.span), + _ => throw SourceSpanException( + 'Invalid public key value.', publicKeyNode.span), + }; + return PrecompiledBinaries( + uriPrefix: urlPrefix, + publicKey: publicKey, + ); + } + } + throw SourceSpanException( + 'Invalid precompiled binaries value. ' + 'Expected Map with "url_prefix" and "public_key".', + node.span); + } +} + +/// Cargokit options specified for Rust crate. +class CargokitCrateOptions { + CargokitCrateOptions({ + this.cargo = const {}, + this.precompiledBinaries, + }); + + final Map cargo; + final PrecompiledBinaries? precompiledBinaries; + + static CargokitCrateOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargokit options must be a map', node.span); + } + final options = {}; + PrecompiledBinaries? precompiledBinaries; + + for (final entry in node.nodes.entries) { + if (entry + case MapEntry( + key: YamlScalar(value: 'cargo'), + value: YamlNode node, + )) { + if (node is! YamlMap) { + throw SourceSpanException('Cargo options must be a map', node.span); + } + for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) { + if (key case YamlScalar(value: String name)) { + final configuration = BuildConfiguration.values + .firstWhereOrNull((element) => element.name == name); + if (configuration != null) { + options[configuration] = CargoBuildOptions.parse(value); + continue; + } + } + throw SourceSpanException( + 'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.', + key.span); + } + } else if (entry.key case YamlScalar(value: 'precompiled_binaries')) { + precompiledBinaries = PrecompiledBinaries.parse(entry.value); + } else { + throw SourceSpanException( + 'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".', + entry.key.span); + } + } + return CargokitCrateOptions( + cargo: options, + precompiledBinaries: precompiledBinaries, + ); + } + + static CargokitCrateOptions load({ + required String manifestDir, + }) { + final uri = Uri.file(path.join(manifestDir, "cargokit.yaml")); + final file = File.fromUri(uri); + if (file.existsSync()) { + final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri); + return parse(contents); + } else { + return CargokitCrateOptions(); + } + } +} + +class CargokitUserOptions { + // When Rustup is installed always build locally unless user opts into + // using precompiled binaries. + static bool defaultUsePrecompiledBinaries() { + return Rustup.executablePath() == null; + } + + CargokitUserOptions({ + required this.usePrecompiledBinaries, + required this.verboseLogging, + }); + + CargokitUserOptions._() + : usePrecompiledBinaries = defaultUsePrecompiledBinaries(), + verboseLogging = false; + + static CargokitUserOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargokit options must be a map', node.span); + } + bool usePrecompiledBinaries = defaultUsePrecompiledBinaries(); + bool verboseLogging = false; + + for (final entry in node.nodes.entries) { + if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) { + if (entry.value case YamlScalar(value: bool value)) { + usePrecompiledBinaries = value; + continue; + } + throw SourceSpanException( + 'Invalid value for "use_precompiled_binaries". Must be a boolean.', + entry.value.span); + } else if (entry.key case YamlScalar(value: 'verbose_logging')) { + if (entry.value case YamlScalar(value: bool value)) { + verboseLogging = value; + continue; + } + throw SourceSpanException( + 'Invalid value for "verbose_logging". Must be a boolean.', + entry.value.span); + } else { + throw SourceSpanException( + 'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".', + entry.key.span); + } + } + return CargokitUserOptions( + usePrecompiledBinaries: usePrecompiledBinaries, + verboseLogging: verboseLogging, + ); + } + + static CargokitUserOptions load() { + String fileName = "cargokit_options.yaml"; + var userProjectDir = Directory(Environment.rootProjectDir); + + while (userProjectDir.parent.path != userProjectDir.path) { + final configFile = File(path.join(userProjectDir.path, fileName)); + if (configFile.existsSync()) { + final contents = loadYamlNode( + configFile.readAsStringSync(), + sourceUrl: configFile.uri, + ); + final res = parse(contents); + if (res.verboseLogging) { + _log.info('Found user options file at ${configFile.path}'); + } + return res; + } + userProjectDir = userProjectDir.parent; + } + return CargokitUserOptions._(); + } + + final bool usePrecompiledBinaries; + final bool verboseLogging; +} diff --git a/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart b/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart new file mode 100644 index 0000000..c27f419 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/precompile_binaries.dart @@ -0,0 +1,202 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:github/github.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'cargo.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'rustup.dart'; +import 'target.dart'; + +final _log = Logger('precompile_binaries'); + +class PrecompileBinaries { + PrecompileBinaries({ + required this.privateKey, + required this.githubToken, + required this.repositorySlug, + required this.manifestDir, + required this.targets, + this.androidSdkLocation, + this.androidNdkVersion, + this.androidMinSdkVersion, + this.tempDir, + }); + + final PrivateKey privateKey; + final String githubToken; + final RepositorySlug repositorySlug; + final String manifestDir; + final List targets; + final String? androidSdkLocation; + final String? androidNdkVersion; + final int? androidMinSdkVersion; + final String? tempDir; + + static String fileName(Target target, String name) { + return '${target.rust}_$name'; + } + + static String signatureFileName(Target target, String name) { + return '${target.rust}_$name.sig'; + } + + Future run() async { + final crateInfo = CrateInfo.load(manifestDir); + + final targets = List.of(this.targets); + if (targets.isEmpty) { + targets.addAll([ + ...Target.buildableTargets(), + if (androidSdkLocation != null) ...Target.androidTargets(), + ]); + } + + _log.info('Precompiling binaries for $targets'); + + final hash = CrateHash.compute(manifestDir); + _log.info('Computed crate hash: $hash'); + + final String tagName = 'precompiled_$hash'; + + final github = GitHub(auth: Authentication.withToken(githubToken)); + final repo = github.repositories; + final release = await _getOrCreateRelease( + repo: repo, + tagName: tagName, + packageName: crateInfo.packageName, + hash: hash, + ); + + final tempDir = this.tempDir != null + ? Directory(this.tempDir!) + : Directory.systemTemp.createTempSync('precompiled_'); + + tempDir.createSync(recursive: true); + + final crateOptions = CargokitCrateOptions.load( + manifestDir: manifestDir, + ); + + final buildEnvironment = BuildEnvironment( + configuration: BuildConfiguration.release, + crateOptions: crateOptions, + targetTempDir: tempDir.path, + manifestDir: manifestDir, + crateInfo: crateInfo, + isAndroid: androidSdkLocation != null, + androidSdkPath: androidSdkLocation, + androidNdkVersion: androidNdkVersion, + androidMinSdkVersion: androidMinSdkVersion, + ); + + final rustup = Rustup(); + + for (final target in targets) { + final artifactNames = getArtifactNames( + target: target, + libraryName: crateInfo.packageName, + remote: true, + ); + + if (artifactNames.every((name) { + final fileName = PrecompileBinaries.fileName(target, name); + return (release.assets ?? []).any((e) => e.name == fileName); + })) { + _log.info("All artifacts for $target already exist - skipping"); + continue; + } + + _log.info('Building for $target'); + + final builder = + RustBuilder(target: target, environment: buildEnvironment); + builder.prepare(rustup); + final res = await builder.build(); + + final assets = []; + for (final name in artifactNames) { + final file = File(path.join(res, name)); + if (!file.existsSync()) { + throw Exception('Missing artifact: ${file.path}'); + } + + final data = file.readAsBytesSync(); + final create = CreateReleaseAsset( + name: PrecompileBinaries.fileName(target, name), + contentType: "application/octet-stream", + assetData: data, + ); + final signature = sign(privateKey, data); + final signatureCreate = CreateReleaseAsset( + name: signatureFileName(target, name), + contentType: "application/octet-stream", + assetData: signature, + ); + bool verified = verify(public(privateKey), data, signature); + if (!verified) { + throw Exception('Signature verification failed'); + } + assets.add(create); + assets.add(signatureCreate); + } + _log.info('Uploading assets: ${assets.map((e) => e.name)}'); + for (final asset in assets) { + // This seems to be failing on CI so do it one by one + int retryCount = 0; + while (true) { + try { + await repo.uploadReleaseAssets(release, [asset]); + break; + } on Exception catch (e) { + if (retryCount == 10) { + rethrow; + } + ++retryCount; + _log.shout( + 'Upload failed (attempt $retryCount, will retry): ${e.toString()}'); + await Future.delayed(Duration(seconds: 2)); + } + } + } + } + + _log.info('Cleaning up'); + tempDir.deleteSync(recursive: true); + } + + Future _getOrCreateRelease({ + required RepositoriesService repo, + required String tagName, + required String packageName, + required String hash, + }) async { + Release release; + try { + _log.info('Fetching release $tagName'); + release = await repo.getReleaseByTagName(repositorySlug, tagName); + } on ReleaseNotFound { + _log.info('Release not found - creating release $tagName'); + release = await repo.createRelease( + repositorySlug, + CreateRelease.from( + tagName: tagName, + name: 'Precompiled binaries ${hash.substring(0, 8)}', + targetCommitish: null, + isDraft: false, + isPrerelease: false, + body: 'Precompiled binaries for crate $packageName, ' + 'crate hash $hash.', + )); + } + return release; + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/rustup.dart b/rust_builder/cargokit/build_tool/lib/src/rustup.dart new file mode 100644 index 0000000..0ac8d08 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/rustup.dart @@ -0,0 +1,136 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as path; + +import 'util.dart'; + +class _Toolchain { + _Toolchain( + this.name, + this.targets, + ); + + final String name; + final List targets; +} + +class Rustup { + List? installedTargets(String toolchain) { + final targets = _installedTargets(toolchain); + return targets != null ? List.unmodifiable(targets) : null; + } + + void installToolchain(String toolchain) { + log.info("Installing Rust toolchain: $toolchain"); + runCommand("rustup", ['toolchain', 'install', toolchain]); + _installedToolchains + .add(_Toolchain(toolchain, _getInstalledTargets(toolchain))); + } + + void installTarget( + String target, { + required String toolchain, + }) { + log.info("Installing Rust target: $target"); + runCommand("rustup", [ + 'target', + 'add', + '--toolchain', + toolchain, + target, + ]); + _installedTargets(toolchain)?.add(target); + } + + final List<_Toolchain> _installedToolchains; + + Rustup() : _installedToolchains = _getInstalledToolchains(); + + List? _installedTargets(String toolchain) => _installedToolchains + .firstWhereOrNull( + (e) => e.name == toolchain || e.name.startsWith('$toolchain-')) + ?.targets; + + static List<_Toolchain> _getInstalledToolchains() { + String extractToolchainName(String line) { + // ignore (default) after toolchain name + final parts = line.split(' '); + return parts[0]; + } + + final res = runCommand("rustup", ['toolchain', 'list']); + + // To list all non-custom toolchains, we need to filter out lines that + // don't start with "stable", "beta", or "nightly". + Pattern nonCustom = RegExp(r"^(stable|beta|nightly)"); + final lines = res.stdout + .toString() + .split('\n') + .where((e) => e.isNotEmpty && e.startsWith(nonCustom)) + .map(extractToolchainName) + .toList(growable: true); + + return lines + .map( + (name) => _Toolchain( + name, + _getInstalledTargets(name), + ), + ) + .toList(growable: true); + } + + static List _getInstalledTargets(String toolchain) { + final res = runCommand("rustup", [ + 'target', + 'list', + '--toolchain', + toolchain, + '--installed', + ]); + final lines = res.stdout + .toString() + .split('\n') + .where((e) => e.isNotEmpty) + .toList(growable: true); + return lines; + } + + bool _didInstallRustSrcForNightly = false; + + void installRustSrcForNightly() { + if (_didInstallRustSrcForNightly) { + return; + } + // Useful for -Z build-std + runCommand( + "rustup", + ['component', 'add', 'rust-src', '--toolchain', 'nightly'], + ); + _didInstallRustSrcForNightly = true; + } + + static String? executablePath() { + final envPath = Platform.environment['PATH']; + final envPathSeparator = Platform.isWindows ? ';' : ':'; + final home = Platform.isWindows + ? Platform.environment['USERPROFILE'] + : Platform.environment['HOME']; + final paths = [ + if (home != null) path.join(home, '.cargo', 'bin'), + if (envPath != null) ...envPath.split(envPathSeparator), + ]; + for (final p in paths) { + final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup'; + final rustupPath = path.join(p, rustup); + if (File(rustupPath).existsSync()) { + return rustupPath; + } + } + return null; + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/target.dart b/rust_builder/cargokit/build_tool/lib/src/target.dart new file mode 100644 index 0000000..6fbc58b --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/target.dart @@ -0,0 +1,140 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; + +import 'util.dart'; + +class Target { + Target({ + required this.rust, + this.flutter, + this.android, + this.androidMinSdkVersion, + this.darwinPlatform, + this.darwinArch, + }); + + static final all = [ + Target( + rust: 'armv7-linux-androideabi', + flutter: 'android-arm', + android: 'armeabi-v7a', + androidMinSdkVersion: 16, + ), + Target( + rust: 'aarch64-linux-android', + flutter: 'android-arm64', + android: 'arm64-v8a', + androidMinSdkVersion: 21, + ), + Target( + rust: 'i686-linux-android', + flutter: 'android-x86', + android: 'x86', + androidMinSdkVersion: 16, + ), + Target( + rust: 'x86_64-linux-android', + flutter: 'android-x64', + android: 'x86_64', + androidMinSdkVersion: 21, + ), + Target( + rust: 'x86_64-pc-windows-msvc', + flutter: 'windows-x64', + ), + Target( + rust: 'x86_64-unknown-linux-gnu', + flutter: 'linux-x64', + ), + Target( + rust: 'aarch64-unknown-linux-gnu', + flutter: 'linux-arm64', + ), + Target( + rust: 'x86_64-apple-darwin', + darwinPlatform: 'macosx', + darwinArch: 'x86_64', + ), + Target( + rust: 'aarch64-apple-darwin', + darwinPlatform: 'macosx', + darwinArch: 'arm64', + ), + Target( + rust: 'aarch64-apple-ios', + darwinPlatform: 'iphoneos', + darwinArch: 'arm64', + ), + Target( + rust: 'aarch64-apple-ios-sim', + darwinPlatform: 'iphonesimulator', + darwinArch: 'arm64', + ), + Target( + rust: 'x86_64-apple-ios', + darwinPlatform: 'iphonesimulator', + darwinArch: 'x86_64', + ), + ]; + + static Target? forFlutterName(String flutterName) { + return all.firstWhereOrNull((element) => element.flutter == flutterName); + } + + static Target? forDarwin({ + required String platformName, + required String darwinAarch, + }) { + return all.firstWhereOrNull((element) => // + element.darwinPlatform == platformName && + element.darwinArch == darwinAarch); + } + + static Target? forRustTriple(String triple) { + return all.firstWhereOrNull((element) => element.rust == triple); + } + + static List androidTargets() { + return all + .where((element) => element.android != null) + .toList(growable: false); + } + + /// Returns buildable targets on current host platform ignoring Android targets. + static List buildableTargets() { + if (Platform.isLinux) { + // Right now we don't support cross-compiling on Linux. So we just return + // the host target. + final arch = runCommand('arch', []).stdout as String; + if (arch.trim() == 'aarch64') { + return [Target.forRustTriple('aarch64-unknown-linux-gnu')!]; + } else { + return [Target.forRustTriple('x86_64-unknown-linux-gnu')!]; + } + } + return all.where((target) { + if (Platform.isWindows) { + return target.rust.contains('-windows-'); + } else if (Platform.isMacOS) { + return target.darwinPlatform != null; + } + return false; + }).toList(growable: false); + } + + @override + String toString() { + return rust; + } + + final String? flutter; + final String rust; + final String? android; + final int? androidMinSdkVersion; + final String? darwinPlatform; + final String? darwinArch; +} diff --git a/rust_builder/cargokit/build_tool/lib/src/util.dart b/rust_builder/cargokit/build_tool/lib/src/util.dart new file mode 100644 index 0000000..8bb6a87 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/util.dart @@ -0,0 +1,172 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'logging.dart'; +import 'rustup.dart'; + +final log = Logger("process"); + +class CommandFailedException implements Exception { + final String executable; + final List arguments; + final ProcessResult result; + + CommandFailedException({ + required this.executable, + required this.arguments, + required this.result, + }); + + @override + String toString() { + final stdout = result.stdout.toString().trim(); + final stderr = result.stderr.toString().trim(); + return [ + "External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}", + "Returned Exit Code: ${result.exitCode}", + kSeparator, + "STDOUT:", + if (stdout.isNotEmpty) stdout, + kSeparator, + "STDERR:", + if (stderr.isNotEmpty) stderr, + ].join('\n'); + } +} + +class TestRunCommandArgs { + final String executable; + final List arguments; + final String? workingDirectory; + final Map? environment; + final bool includeParentEnvironment; + final bool runInShell; + final Encoding? stdoutEncoding; + final Encoding? stderrEncoding; + + TestRunCommandArgs({ + required this.executable, + required this.arguments, + this.workingDirectory, + this.environment, + this.includeParentEnvironment = true, + this.runInShell = false, + this.stdoutEncoding, + this.stderrEncoding, + }); +} + +class TestRunCommandResult { + TestRunCommandResult({ + this.pid = 1, + this.exitCode = 0, + this.stdout = '', + this.stderr = '', + }); + + final int pid; + final int exitCode; + final String stdout; + final String stderr; +} + +TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride; + +ProcessResult runCommand( + String executable, + List arguments, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding? stdoutEncoding = systemEncoding, + Encoding? stderrEncoding = systemEncoding, +}) { + if (testRunCommandOverride != null) { + final result = testRunCommandOverride!(TestRunCommandArgs( + executable: executable, + arguments: arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding, + )); + return ProcessResult( + result.pid, + result.exitCode, + result.stdout, + result.stderr, + ); + } + log.finer('Running command $executable ${arguments.join(' ')}'); + final res = Process.runSync( + _resolveExecutable(executable), + arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stderrEncoding: stderrEncoding, + stdoutEncoding: stdoutEncoding, + ); + if (res.exitCode != 0) { + throw CommandFailedException( + executable: executable, + arguments: arguments, + result: res, + ); + } else { + return res; + } +} + +class RustupNotFoundException implements Exception { + @override + String toString() { + return [ + ' ', + 'rustup not found in PATH.', + ' ', + 'Maybe you need to install Rust? It only takes a minute:', + ' ', + if (Platform.isWindows) 'https://www.rust-lang.org/tools/install', + if (hasHomebrewRustInPath()) ...[ + '\$ brew unlink rust # Unlink homebrew Rust from PATH', + ], + if (!Platform.isWindows) + "\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh", + ' ', + ].join('\n'); + } + + static bool hasHomebrewRustInPath() { + if (!Platform.isMacOS) { + return false; + } + final envPath = Platform.environment['PATH'] ?? ''; + final paths = envPath.split(':'); + return paths.any((p) { + return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync(); + }); + } +} + +String _resolveExecutable(String executable) { + if (executable == 'rustup') { + final resolved = Rustup.executablePath(); + if (resolved != null) { + return resolved; + } + throw RustupNotFoundException(); + } else { + return executable; + } +} diff --git a/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart b/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart new file mode 100644 index 0000000..2366b57 --- /dev/null +++ b/rust_builder/cargokit/build_tool/lib/src/verify_binaries.dart @@ -0,0 +1,84 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:http/http.dart'; + +import 'artifacts_provider.dart'; +import 'cargo.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'target.dart'; + +class VerifyBinaries { + VerifyBinaries({ + required this.manifestDir, + }); + + final String manifestDir; + + Future run() async { + final crateInfo = CrateInfo.load(manifestDir); + + final config = CargokitCrateOptions.load(manifestDir: manifestDir); + final precompiledBinaries = config.precompiledBinaries; + if (precompiledBinaries == null) { + stdout.writeln('Crate does not support precompiled binaries.'); + } else { + final crateHash = CrateHash.compute(manifestDir); + stdout.writeln('Crate hash: $crateHash'); + + for (final target in Target.all) { + final message = 'Checking ${target.rust}...'; + stdout.write(message.padRight(40)); + stdout.flush(); + + final artifacts = getArtifactNames( + target: target, + libraryName: crateInfo.packageName, + remote: true, + ); + + final prefix = precompiledBinaries.uriPrefix; + + bool ok = true; + + for (final artifact in artifacts) { + final fileName = PrecompileBinaries.fileName(target, artifact); + final signatureFileName = + PrecompileBinaries.signatureFileName(target, artifact); + + final url = Uri.parse('$prefix$crateHash/$fileName'); + final signatureUrl = + Uri.parse('$prefix$crateHash/$signatureFileName'); + + final signature = await get(signatureUrl); + if (signature.statusCode != 200) { + stdout.writeln('MISSING'); + ok = false; + break; + } + final asset = await get(url); + if (asset.statusCode != 200) { + stdout.writeln('MISSING'); + ok = false; + break; + } + + if (!verify(precompiledBinaries.publicKey, asset.bodyBytes, + signature.bodyBytes)) { + stdout.writeln('INVALID SIGNATURE'); + ok = false; + } + } + + if (ok) { + stdout.writeln('OK'); + } + } + } + } +} diff --git a/rust_builder/cargokit/build_tool/pubspec.lock b/rust_builder/cargokit/build_tool/pubspec.lock new file mode 100644 index 0000000..343bdd3 --- /dev/null +++ b/rust_builder/cargokit/build_tool/pubspec.lock @@ -0,0 +1,453 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + args: + dependency: "direct main" + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: "direct main" + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + url: "https://pub.dev" + source: hosted + version: "1.6.3" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + ed25519_edwards: + dependency: "direct main" + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + github: + dependency: "direct main" + description: + name: github + sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411" + url: "https://pub.dev" + source: hosted + version: "9.17.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + hex: + dependency: "direct main" + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + http: + dependency: "direct main" + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: "direct main" + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: "direct main" + description: + name: path + sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb" + url: "https://pub.dev" + source: hosted + version: "1.8.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: "direct main" + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9" + url: "https://pub.dev" + source: hosted + version: "1.24.6" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265" + url: "https://pub.dev" + source: hosted + version: "0.5.6" + toml: + dependency: "direct main" + description: + name: toml + sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44" + url: "https://pub.dev" + source: hosted + version: "0.14.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + version: + dependency: "direct main" + description: + name: version + sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" + url: "https://pub.dev" + source: hosted + version: "11.9.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + yaml: + dependency: "direct main" + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.0.0 <4.0.0" diff --git a/rust_builder/cargokit/build_tool/pubspec.yaml b/rust_builder/cargokit/build_tool/pubspec.yaml new file mode 100644 index 0000000..18c61e3 --- /dev/null +++ b/rust_builder/cargokit/build_tool/pubspec.yaml @@ -0,0 +1,33 @@ +# This is copied from Cargokit (which is the official way to use it currently) +# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +name: build_tool +description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build. +publish_to: none +version: 1.0.0 + +environment: + sdk: ">=3.0.0 <4.0.0" + +# Add regular dependencies here. +dependencies: + # these are pinned on purpose because the bundle_tool_runner doesn't have + # pubspec.lock. See run_build_tool.sh + logging: 1.2.0 + path: 1.8.0 + version: 3.0.0 + collection: 1.18.0 + ed25519_edwards: 0.3.1 + hex: 0.2.0 + yaml: 3.1.2 + source_span: 1.10.0 + github: 9.17.0 + args: 2.4.2 + crypto: 3.0.3 + convert: 3.1.1 + http: 1.1.0 + toml: 0.14.0 + +dev_dependencies: + lints: ^2.1.0 + test: ^1.24.0 diff --git a/rust_builder/cargokit/cmake/cargokit.cmake b/rust_builder/cargokit/cmake/cargokit.cmake new file mode 100644 index 0000000..ddd05df --- /dev/null +++ b/rust_builder/cargokit/cmake/cargokit.cmake @@ -0,0 +1,99 @@ +SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..") + +# Workaround for https://github.com/dart-lang/pub/issues/4010 +get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH) + +if(WIN32) + # REALPATH does not properly resolve symlinks on windows :-/ + execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE) +endif() + +# Arguments +# - target: CMAKE target to which rust library is linked +# - manifest_dir: relative path from current folder to directory containing cargo manifest +# - lib_name: cargo package name +# - any_symbol_name: name of any exported symbol from the library. +# used on windows to force linking with library. +function(apply_cargokit target manifest_dir lib_name any_symbol_name) + + set(CARGOKIT_LIB_NAME "${lib_name}") + set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}") + if (CMAKE_CONFIGURATION_TYPES) + set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$") + set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$/${CARGOKIT_LIB_FULL_NAME}") + else() + set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") + set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}") + endif() + set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build") + + if (FLUTTER_TARGET_PLATFORM) + set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}") + else() + set(CARGOKIT_TARGET_PLATFORM "windows-x64") + endif() + + set(CARGOKIT_ENV + "CARGOKIT_CMAKE=${CMAKE_COMMAND}" + "CARGOKIT_CONFIGURATION=$" + "CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}" + "CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}" + "CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}" + "CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}" + "CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool" + "CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}" + ) + + if (WIN32) + set(SCRIPT_EXTENSION ".cmd") + set(IMPORT_LIB_EXTENSION ".lib") + else() + set(SCRIPT_EXTENSION ".sh") + set(IMPORT_LIB_EXTENSION "") + execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}") + endif() + + # Using generators in custom command is only supported in CMake 3.20+ + if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0") + foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES) + add_custom_command( + OUTPUT + "${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}" + "${CMAKE_CURRENT_BINARY_DIR}/_phony_" + COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} + "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake + VERBATIM + ) + endforeach() + else() + add_custom_command( + OUTPUT + ${OUTPUT_LIB} + "${CMAKE_CURRENT_BINARY_DIR}/_phony_" + COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} + "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake + VERBATIM + ) + endif() + + + set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE) + + if (TARGET ${target}) + # If we have actual cmake target provided create target and make existing + # target depend on it + add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB}) + add_dependencies("${target}" "${target}_cargokit") + target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}") + if(WIN32) + target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}") + endif() + else() + # Otherwise (FFI) just use ALL to force building always + add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB}) + endif() + + # Allow adding the output library to plugin bundled libraries + set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE) + +endfunction() diff --git a/rust_builder/cargokit/cmake/resolve_symlinks.ps1 b/rust_builder/cargokit/cmake/resolve_symlinks.ps1 new file mode 100644 index 0000000..3d10d28 --- /dev/null +++ b/rust_builder/cargokit/cmake/resolve_symlinks.ps1 @@ -0,0 +1,27 @@ +function Resolve-Symlinks { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [string] $Path + ) + + [string] $separator = '/' + [string[]] $parts = $Path.Split($separator) + + [string] $realPath = '' + foreach ($part in $parts) { + if ($realPath -and !$realPath.EndsWith($separator)) { + $realPath += $separator + } + $realPath += $part + $item = Get-Item $realPath + if ($item.Target) { + $realPath = $item.Target.Replace('\', '/') + } + } + $realPath +} + +$path=Resolve-Symlinks -Path $args[0] +Write-Host $path diff --git a/rust_builder/cargokit/gradle/plugin.gradle b/rust_builder/cargokit/gradle/plugin.gradle new file mode 100644 index 0000000..1aead89 --- /dev/null +++ b/rust_builder/cargokit/gradle/plugin.gradle @@ -0,0 +1,179 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import java.nio.file.Paths +import org.apache.tools.ant.taskdefs.condition.Os + +CargoKitPlugin.file = buildscript.sourceFile + +apply plugin: CargoKitPlugin + +class CargoKitExtension { + String manifestDir; // Relative path to folder containing Cargo.toml + String libname; // Library name within Cargo.toml. Must be a cdylib +} + +abstract class CargoKitBuildTask extends DefaultTask { + + @Input + String buildMode + + @Input + String buildDir + + @Input + String outputDir + + @Input + String ndkVersion + + @Input + String sdkDirectory + + @Input + int compileSdkVersion; + + @Input + int minSdkVersion; + + @Input + String pluginFile + + @Input + List targetPlatforms + + @TaskAction + def build() { + if (project.cargokit.manifestDir == null) { + throw new GradleException("Property 'manifestDir' must be set on cargokit extension"); + } + + if (project.cargokit.libname == null) { + throw new GradleException("Property 'libname' must be set on cargokit extension"); + } + + def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh" + def path = Paths.get(new File(pluginFile).parent, "..", executableName); + + def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir) + + def rootProjectDir = project.rootProject.projectDir + + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + project.exec { + commandLine 'chmod', '+x', path + } + } + + project.exec { + executable path + args "build-gradle" + environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir + environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool" + environment "CARGOKIT_MANIFEST_DIR", manifestDir + environment "CARGOKIT_CONFIGURATION", buildMode + environment "CARGOKIT_TARGET_TEMP_DIR", buildDir + environment "CARGOKIT_OUTPUT_DIR", outputDir + environment "CARGOKIT_NDK_VERSION", ndkVersion + environment "CARGOKIT_SDK_DIR", sdkDirectory + environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion + environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion + environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",") + environment "CARGOKIT_JAVA_HOME", System.properties['java.home'] + } + } +} + +class CargoKitPlugin implements Plugin { + + static String file; + + private Plugin findFlutterPlugin(Project rootProject) { + _findFlutterPlugin(rootProject.childProjects) + } + + private Plugin _findFlutterPlugin(Map projects) { + for (project in projects) { + for (plugin in project.value.getPlugins()) { + if (plugin.class.name == "FlutterPlugin") { + return plugin; + } + } + def plugin = _findFlutterPlugin(project.value.childProjects); + if (plugin != null) { + return plugin; + } + } + return null; + } + + @Override + void apply(Project project) { + def plugin = findFlutterPlugin(project.rootProject); + + project.extensions.create("cargokit", CargoKitExtension) + + if (plugin == null) { + print("Flutter plugin not found, CargoKit plugin will not be applied.") + return; + } + + def cargoBuildDir = "${project.buildDir}/build" + + // Determine if the project is an application or library + def isApplication = plugin.project.plugins.hasPlugin('com.android.application') + def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants + + variants.all { variant -> + + final buildType = variant.buildType.name + + def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}"; + def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs; + jniLibs.srcDir(new File(cargoOutputDir)) + + def platforms = plugin.getTargetPlatforms().collect() + + // Same thing addFlutterDependencies does in flutter.gradle + if (buildType == "debug") { + platforms.add("android-x86") + platforms.add("android-x64") + } + + // The task name depends on plugin properties, which are not available + // at this point + project.getGradle().afterProject { + def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}"; + + if (project.tasks.findByName(taskName)) { + return + } + + if (plugin.project.android.ndkVersion == null) { + throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.") + } + + def task = project.tasks.create(taskName, CargoKitBuildTask.class) { + buildMode = variant.buildType.name + buildDir = cargoBuildDir + outputDir = cargoOutputDir + ndkVersion = plugin.project.android.ndkVersion + sdkDirectory = plugin.project.android.sdkDirectory + minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int + compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int + targetPlatforms = platforms + pluginFile = CargoKitPlugin.file + } + def onTask = { newTask -> + if (newTask.name == "merge${buildType.capitalize()}NativeLibs") { + newTask.dependsOn task + // Fix gradle 7.4.2 not picking up JNI library changes + newTask.outputs.upToDateWhen { false } + } + } + project.tasks.each onTask + project.tasks.whenTaskAdded onTask + } + } + } +} diff --git a/rust_builder/cargokit/run_build_tool.cmd b/rust_builder/cargokit/run_build_tool.cmd new file mode 100644 index 0000000..c45d0aa --- /dev/null +++ b/rust_builder/cargokit/run_build_tool.cmd @@ -0,0 +1,91 @@ +@echo off +setlocal + +setlocal ENABLEDELAYEDEXPANSION + +SET BASEDIR=%~dp0 + +if not exist "%CARGOKIT_TOOL_TEMP_DIR%" ( + mkdir "%CARGOKIT_TOOL_TEMP_DIR%" +) +cd /D "%CARGOKIT_TOOL_TEMP_DIR%" + +SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool +SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart + +set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/% + +( + echo name: build_tool_runner + echo version: 1.0.0 + echo publish_to: none + echo. + echo environment: + echo sdk: '^>=3.0.0 ^<4.0.0' + echo. + echo dependencies: + echo build_tool: + echo path: %BUILD_TOOL_PKG_DIR_POSIX% +) >pubspec.yaml + +if not exist bin ( + mkdir bin +) + +( + echo import 'package:build_tool/build_tool.dart' as build_tool; + echo void main^(List^ args^) ^{ + echo build_tool.runMain^(args^); + echo ^} +) >bin\build_tool_runner.dart + +SET PRECOMPILED=bin\build_tool_runner.dill + +REM To detect changes in package we compare output of DIR /s (recursive) +set PREV_PACKAGE_INFO=.dart_tool\package_info.prev +set CUR_PACKAGE_INFO=.dart_tool\package_info.cur + +DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig" + +REM Last line in dir output is free space on harddrive. That is bound to +REM change between invocation so we need to remove it +( + Set "Line=" + For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do ( + SetLocal EnableDelayedExpansion + If Defined Line Echo !Line! + EndLocal + Set "Line=%%A") +) >"%CUR_PACKAGE_INFO%" +DEL "%CUR_PACKAGE_INFO%_orig" + +REM Compare current directory listing with previous +FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1 + +If %ERRORLEVEL% neq 0 ( + REM Changed - copy current to previous and remove precompiled kernel + if exist "%PREV_PACKAGE_INFO%" ( + DEL "%PREV_PACKAGE_INFO%" + ) + MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" + if exist "%PRECOMPILED%" ( + DEL "%PRECOMPILED%" + ) +) + +REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO% +REM which means we need to do pub get and precompile +if not exist "%PRECOMPILED%" ( + echo Running pub get in "%cd%" + "%DART%" pub get --no-precompile + "%DART%" compile kernel bin/build_tool_runner.dart +) + +"%DART%" "%PRECOMPILED%" %* + +REM 253 means invalid snapshot version. +If %ERRORLEVEL% equ 253 ( + "%DART%" pub get --no-precompile + "%DART%" compile kernel bin/build_tool_runner.dart + "%DART%" "%PRECOMPILED%" %* +) diff --git a/rust_builder/cargokit/run_build_tool.sh b/rust_builder/cargokit/run_build_tool.sh new file mode 100644 index 0000000..6e594a2 --- /dev/null +++ b/rust_builder/cargokit/run_build_tool.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +set -e + +BASEDIR=$(dirname "$0") + +mkdir -p "$CARGOKIT_TOOL_TEMP_DIR" + +cd "$CARGOKIT_TOOL_TEMP_DIR" + +# Write a very simple bin package in temp folder that depends on build_tool package +# from Cargokit. This is done to ensure that we don't pollute Cargokit folder +# with .dart_tool contents. + +BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool" + +if [[ -z $FLUTTER_ROOT ]]; then # not defined + DART=dart +else + DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart" +fi + +cat << EOF > "pubspec.yaml" +name: build_tool_runner +version: 1.0.0 +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + build_tool: + path: "$BUILD_TOOL_PKG_DIR" +EOF + +mkdir -p "bin" + +cat << EOF > "bin/build_tool_runner.dart" +import 'package:build_tool/build_tool.dart' as build_tool; +void main(List args) { + build_tool.runMain(args); +} +EOF + +# Create alias for `shasum` if it does not exist and `sha1sum` exists +if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then + shopt -s expand_aliases + alias shasum="sha1sum" +fi + +# Dart run will not cache any package that has a path dependency, which +# is the case for our build_tool_runner. So instead we precompile the package +# ourselves. +# To invalidate the cached kernel we use the hash of ls -LR of the build_tool +# package directory. This should be good enough, as the build_tool package +# itself is not meant to have any path dependencies. + +if [[ "$OSTYPE" == "darwin"* ]]; then + PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum) +else + PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum) +fi + +PACKAGE_HASH_FILE=".package_hash" + +if [ -f "$PACKAGE_HASH_FILE" ]; then + EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE") + if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then + rm "$PACKAGE_HASH_FILE" + fi +fi + +# Run pub get if needed. +if [ ! -f "$PACKAGE_HASH_FILE" ]; then + "$DART" pub get --no-precompile + "$DART" compile kernel bin/build_tool_runner.dart + echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE" +fi + +set +e + +"$DART" bin/build_tool_runner.dill "$@" + +exit_code=$? + +# 253 means invalid snapshot version. +if [ $exit_code == 253 ]; then + "$DART" pub get --no-precompile + "$DART" compile kernel bin/build_tool_runner.dart + "$DART" bin/build_tool_runner.dill "$@" + exit_code=$? +fi + +exit $exit_code diff --git a/rust_builder/ios/Classes/dummy_file.c b/rust_builder/ios/Classes/dummy_file.c new file mode 100644 index 0000000..e06dab9 --- /dev/null +++ b/rust_builder/ios/Classes/dummy_file.c @@ -0,0 +1 @@ +// This is an empty file to force CocoaPods to create a framework. diff --git a/rust_builder/ios/rust_lib_fltier.podspec b/rust_builder/ios/rust_lib_fltier.podspec new file mode 100644 index 0000000..28774a8 --- /dev/null +++ b/rust_builder/ios/rust_lib_fltier.podspec @@ -0,0 +1,45 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint rust_lib_fltier.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'rust_lib_fltier' + s.version = '0.0.1' + s.summary = 'A new Flutter FFI plugin project.' + s.description = <<-DESC +A new Flutter FFI plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + + s.script_phase = { + :name => 'Build Rust library', + # First argument is relative path to the `rust` folder, second is name of rust library + :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../rust rust_lib_fltier', + :execution_position => :before_compile, + :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], + # Let XCode know that the static library referenced in -force_load below is + # created by this build step. + :output_files => ["${BUILT_PRODUCTS_DIR}/librust_lib_fltier.a"], + } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + # Flutter.framework does not contain a i386 slice. + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/librust_lib_fltier.a', + } +end \ No newline at end of file diff --git a/rust_builder/linux/CMakeLists.txt b/rust_builder/linux/CMakeLists.txt new file mode 100644 index 0000000..6addbeb --- /dev/null +++ b/rust_builder/linux/CMakeLists.txt @@ -0,0 +1,19 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +# Project-level configuration. +set(PROJECT_NAME "rust_lib_fltier") +project(${PROJECT_NAME} LANGUAGES CXX) + +include("../cargokit/cmake/cargokit.cmake") +apply_cargokit(${PROJECT_NAME} ../../rust rust_lib_fltier "") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(rust_lib_fltier_bundled_libraries + "${${PROJECT_NAME}_cargokit_lib}" + PARENT_SCOPE +) diff --git a/rust_builder/macos/Classes/dummy_file.c b/rust_builder/macos/Classes/dummy_file.c new file mode 100644 index 0000000..e06dab9 --- /dev/null +++ b/rust_builder/macos/Classes/dummy_file.c @@ -0,0 +1 @@ +// This is an empty file to force CocoaPods to create a framework. diff --git a/rust_builder/macos/rust_lib_fltier.podspec b/rust_builder/macos/rust_lib_fltier.podspec new file mode 100644 index 0000000..695b010 --- /dev/null +++ b/rust_builder/macos/rust_lib_fltier.podspec @@ -0,0 +1,44 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint rust_lib_fltier.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'rust_lib_fltier' + s.version = '0.0.1' + s.summary = 'A new Flutter FFI plugin project.' + s.description = <<-DESC +A new Flutter FFI plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' + + s.script_phase = { + :name => 'Build Rust library', + # First argument is relative path to the `rust` folder, second is name of rust library + :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../rust rust_lib_fltier', + :execution_position => :before_compile, + :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], + # Let XCode know that the static library referenced in -force_load below is + # created by this build step. + :output_files => ["${BUILT_PRODUCTS_DIR}/librust_lib_fltier.a"], + } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + # Flutter.framework does not contain a i386 slice. + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/librust_lib_fltier.a', + } +end \ No newline at end of file diff --git a/rust_builder/pubspec.yaml b/rust_builder/pubspec.yaml new file mode 100644 index 0000000..68063f8 --- /dev/null +++ b/rust_builder/pubspec.yaml @@ -0,0 +1,34 @@ +name: rust_lib_fltier +description: "Utility to build Rust code" +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=3.3.0 <4.0.0' + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + ffi: ^2.0.2 + ffigen: ^11.0.0 + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + plugin: + platforms: + android: + ffiPlugin: true + ios: + ffiPlugin: true + linux: + ffiPlugin: true + macos: + ffiPlugin: true + windows: + ffiPlugin: true diff --git a/rust_builder/windows/.gitignore b/rust_builder/windows/.gitignore new file mode 100644 index 0000000..b3eb2be --- /dev/null +++ b/rust_builder/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/rust_builder/windows/CMakeLists.txt b/rust_builder/windows/CMakeLists.txt new file mode 100644 index 0000000..de77ef0 --- /dev/null +++ b/rust_builder/windows/CMakeLists.txt @@ -0,0 +1,20 @@ +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.14 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. +set(PROJECT_NAME "rust_lib_fltier") +project(${PROJECT_NAME} LANGUAGES CXX) + +include("../cargokit/cmake/cargokit.cmake") +apply_cargokit(${PROJECT_NAME} ../../../../../../rust rust_lib_fltier "") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(rust_lib_fltier_bundled_libraries + "${${PROJECT_NAME}_cargokit_lib}" + PARENT_SCOPE +) diff --git a/test_driver/integration_test.dart b/test_driver/integration_test.dart new file mode 100644 index 0000000..b38629c --- /dev/null +++ b/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..d690e35 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(fltier LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "fltier") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..fde4259 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); + SystemTrayPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SystemTrayPlugin")); + TrayManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("TrayManagerPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..5724c92 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + screen_retriever_windows + system_tray + tray_manager + url_launcher_windows + window_manager +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + rust_lib_fltier +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..0aa7a8e --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "fltier" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "fltier" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "fltier.exe" "\0" + VALUE "ProductName", "fltier" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..1a19c9d --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"fltier", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_