mirror of
https://github.com/EasyTier/astral.git
synced 2025-05-19 10:30:24 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
+43
@@ -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
|
||||
@@ -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'
|
||||
Vendored
+58
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
@@ -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:
|
||||
@@ -0,0 +1,4 @@
|
||||
rust_input: crate::api
|
||||
rust_root: rust/
|
||||
dart_output: lib/src/rust
|
||||
default_dart_async: true
|
||||
+156
@@ -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<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
// 应用程序的状态管理类
|
||||
class _MyAppState extends State<MyApp> {
|
||||
// 主题相关的状态变量
|
||||
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<void> 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<void> 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<void> 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<void> setSeedColor(Color color) async {
|
||||
await _prefs.setInt(_keySeedColor, color.value);
|
||||
}
|
||||
|
||||
// 服务器列表设置
|
||||
static const String _keyServerList = 'serverList';
|
||||
List<String> get serverList {
|
||||
final List<String>? value = _prefs.getStringList(_keyServerList);
|
||||
return value?.isNotEmpty == true ? value! : ['public.easytier.net:11010'];
|
||||
}
|
||||
|
||||
Future<void> setServerList(List<String> servers) async {
|
||||
await _prefs.setStringList(_keyServerList, servers);
|
||||
}
|
||||
|
||||
// 当前选中的服务器设置
|
||||
static const String _keyCurrentServer = 'currentServer';
|
||||
String get currentServer {
|
||||
return _prefs.getString(_keyCurrentServer) ?? 'public.easytier.net:11010';
|
||||
}
|
||||
|
||||
Future<void> setCurrentServer(String server) async {
|
||||
await _prefs.setString(_keyCurrentServer, server);
|
||||
}
|
||||
|
||||
// 房间名设置
|
||||
static const String _keyRoomName = 'roomName';
|
||||
String get roomName {
|
||||
return _prefs.getString(_keyRoomName) ?? 'kevin';
|
||||
}
|
||||
|
||||
Future<void> setRoomName(String name) async {
|
||||
await _prefs.setString(_keyRoomName, name);
|
||||
}
|
||||
|
||||
// 房间密码设置
|
||||
static const String _keyRoomPassword = 'roomPassword';
|
||||
String get roomPassword {
|
||||
return _prefs.getString(_keyRoomPassword) ?? 'kevin';
|
||||
}
|
||||
|
||||
Future<void> setRoomPassword(String password) async {
|
||||
await _prefs.setString(_keyRoomPassword, password);
|
||||
}
|
||||
|
||||
// 用户名设置
|
||||
static const String _keyUsername = 'username';
|
||||
String get username {
|
||||
return _prefs.getString(_keyUsername) ?? Platform.localHostname;
|
||||
}
|
||||
|
||||
Future<void> 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<void> setCloseToTray(bool enabled) async {
|
||||
await _prefs.setBool(_keyCloseToTray, enabled);
|
||||
}
|
||||
|
||||
Future<void> setVirtualIP(String ip) async {
|
||||
await _prefs.setString(_keyVirtualIP, ip);
|
||||
}
|
||||
|
||||
// 动态获取IP设置
|
||||
static const String _keyDynamicIP = 'dynamicIP';
|
||||
bool get dynamicIP {
|
||||
return _prefs.getBool(_keyDynamicIP) ?? true;
|
||||
}
|
||||
|
||||
Future<void> setDynamicIP(bool enabled) async {
|
||||
await _prefs.setBool(_keyDynamicIP, enabled);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<int, Widget> _pageInstances = {};
|
||||
|
||||
static List<NavItem> 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
Future<void> 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();
|
||||
});
|
||||
}
|
||||
@@ -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<void> 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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<MainScreen> createState() => _MainScreenState();
|
||||
}
|
||||
|
||||
class _MainScreenState extends State<MainScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
// 缓存所有页面
|
||||
late List<Widget> _pages;
|
||||
late List<NavItem> _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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<InfoPage> createState() => _InfoPageState();
|
||||
}
|
||||
|
||||
class _InfoPageState extends State<InfoPage>
|
||||
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<double>(
|
||||
tween: Tween<double>(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<double>(
|
||||
tween: Tween<double>(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<double>(
|
||||
tween: Tween<double>(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<double>(
|
||||
tween: Tween<double>(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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<RoomPage> createState() => _RoomPageState();
|
||||
}
|
||||
|
||||
class _RoomPageState extends State<RoomPage> {
|
||||
List<PlayerInfo> 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<KM>(
|
||||
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<void> _processNodeData(KM km) async {
|
||||
try {
|
||||
final nodes = await km.nodes; // 获取最新的节点信息
|
||||
|
||||
// 将节点数据转换为PlayerInfo对象
|
||||
List<PlayerInfo> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
bool _notificationsEnabled = true;
|
||||
double _fontSize = 16.0;
|
||||
late List<String> _serverList;
|
||||
late String _currentServer;
|
||||
final _appConfig = AppConfig();
|
||||
bool _closeToTray = false; // 添加关闭进入托盘变量
|
||||
|
||||
String serverIP = "";
|
||||
// 添加 ping 相关状态
|
||||
Map<String, int?> pingResults = {};
|
||||
Map<String, bool> 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<void> _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<void> _showAddServerDialog() async {
|
||||
final controller = TextEditingController();
|
||||
final result = await showDialog<String>(
|
||||
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<void> _showEditServerDialog(int index) async {
|
||||
final controller = TextEditingController(text: _serverList[index]);
|
||||
final result = await showDialog<String>(
|
||||
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<void> _deleteServer(int index) async {
|
||||
final confirm = await showDialog<bool>(
|
||||
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<KM>(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<KM>(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('灰度版本不支持更新')),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<String, dynamic> 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<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
// 定义状态枚举
|
||||
|
||||
// 当前连接状态
|
||||
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<Widget Function(ColorScheme)> _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<KM>(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<KM>(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<KM>(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<KM>(context).virtualIP;
|
||||
_isAutoIP = Provider.of<KM>(context).dynamicIP; // 更新自动IP状态
|
||||
//我的房间
|
||||
roomName = Provider.of<KM>(context).roomName;
|
||||
//我的密码
|
||||
roomPassword = Provider.of<KM>(context).roomPassword;
|
||||
//我的用户名
|
||||
username = Provider.of<KM>(context).username;
|
||||
Serverip = Provider.of<KM>(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<double> 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<Color>(
|
||||
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<KM>(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<KM>(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<String>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -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<PeerInfo>, List<Route>)> getPeersAndRoutes() =>
|
||||
RustLib.instance.api.crateApiSimpleGetPeersAndRoutes();
|
||||
|
||||
Future<List<PeerRoutePair>> getPeerRoutePairs() =>
|
||||
RustLib.instance.api.crateApiSimpleGetPeerRoutePairs();
|
||||
|
||||
Future<MyNodeInfo> getNodeInfo() =>
|
||||
RustLib.instance.api.crateApiSimpleGetNodeInfo();
|
||||
|
||||
Future<String> easytierVersion() =>
|
||||
RustLib.instance.api.crateApiSimpleEasytierVersion();
|
||||
|
||||
Future<bool> isEasytierRunning() =>
|
||||
RustLib.instance.api.crateApiSimpleIsEasytierRunning();
|
||||
|
||||
Future<KVNetworkStatus> getNetworkStatus() =>
|
||||
RustLib.instance.api.crateApiSimpleGetNetworkStatus();
|
||||
|
||||
Future<String> getRunningInfo() =>
|
||||
RustLib.instance.api.crateApiSimpleGetRunningInfo();
|
||||
|
||||
Future<void> 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<void> closeAllServer() =>
|
||||
RustLib.instance.api.crateApiSimpleCloseAllServer();
|
||||
|
||||
Future<void> initApp() => RustLib.instance.api.crateApiSimpleInitApp();
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<MyNodeInfo>>
|
||||
abstract class MyNodeInfo implements RustOpaqueInterface {}
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PeerInfo>>
|
||||
abstract class PeerInfo implements RustOpaqueInterface {}
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PeerRoutePair>>
|
||||
abstract class PeerRoutePair implements RustOpaqueInterface {}
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<Route>>
|
||||
abstract class Route implements RustOpaqueInterface {}
|
||||
|
||||
class KVNetworkStatus {
|
||||
final BigInt totalNodes;
|
||||
final List<KVNodeInfo> 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<KVNodeConnectionStats> 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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<RustLibWire> {
|
||||
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<PeerInfo>
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<PeerRoutePair>
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<Route>
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> dco_decode_list_kv_node_connection_stats(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<KVNodeInfo> dco_decode_list_kv_node_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||
|
||||
@protected
|
||||
(
|
||||
List<PeerInfo>,
|
||||
List<Route>
|
||||
) 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<PeerInfo>
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<PeerRoutePair>
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<Route>
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> sse_decode_list_kv_node_connection_stats(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<KVNodeInfo> sse_decode_list_kv_node_info(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
(
|
||||
List<PeerInfo>,
|
||||
List<Route>
|
||||
) 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<PeerInfo> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
List<PeerRoutePair> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
List<Route> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_kv_node_connection_stats(
|
||||
List<KVNodeConnectionStats> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_kv_node_info(
|
||||
List<KVNodeInfo> 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<PeerInfo>, List<Route>) 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<T> Function<T extends ffi.NativeType>(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<ffi.Void> 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<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
|
||||
'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 Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMyNodeInfo(
|
||||
ffi.Pointer<ffi.Void> 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<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
|
||||
'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 Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
ffi.Pointer<ffi.Void> 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<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
|
||||
'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 Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
ffi.Pointer<ffi.Void> 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<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
|
||||
'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 Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
ffi.Pointer<ffi.Void> 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<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
|
||||
'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 Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
ffi.Pointer<ffi.Void> 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<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
|
||||
'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 Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
ffi.Pointer<ffi.Void> 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<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
|
||||
'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 Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
ffi.Pointer<ffi.Void> 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<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
|
||||
'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<void Function(ffi.Pointer<ffi.Void>)>();
|
||||
}
|
||||
@@ -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<RustLibWire> {
|
||||
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<PeerInfo>
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<PeerRoutePair>
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<Route>
|
||||
dco_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> dco_decode_list_kv_node_connection_stats(
|
||||
dynamic raw);
|
||||
|
||||
@protected
|
||||
List<KVNodeInfo> dco_decode_list_kv_node_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||
|
||||
@protected
|
||||
(
|
||||
List<PeerInfo>,
|
||||
List<Route>
|
||||
) 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<PeerInfo>
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerInfo(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<PeerRoutePair>
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<Route>
|
||||
sse_decode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<KVNodeConnectionStats> sse_decode_list_kv_node_connection_stats(
|
||||
SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<KVNodeInfo> sse_decode_list_kv_node_info(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
(
|
||||
List<PeerInfo>,
|
||||
List<Route>
|
||||
) 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<PeerInfo> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPeerRoutePair(
|
||||
List<PeerRoutePair> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRoute(
|
||||
List<Route> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_kv_node_connection_stats(
|
||||
List<KVNodeConnectionStats> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_kv_node_info(
|
||||
List<KVNodeInfo> 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<PeerInfo>, List<Route>) 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);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class AppInfoUtil {
|
||||
static PackageInfo? _packageInfo;
|
||||
|
||||
/// 初始化应用信息
|
||||
static Future<void> 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 ?? '';
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
class PingUtil {
|
||||
static Future<int?> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, dynamic> json) =>
|
||||
_$Ipv4AddrFromJson(json);
|
||||
Map<String, dynamic> 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<String, dynamic> json) =>
|
||||
_$Ipv4InetFromJson(json);
|
||||
Map<String, dynamic> 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<String> 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<String, dynamic> json) =>
|
||||
_$StunInfoFromJson(json);
|
||||
Map<String, dynamic> 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<String, dynamic> json) =>
|
||||
_$PeerFeatureFlagFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PeerFeatureFlagToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Url {
|
||||
final String url;
|
||||
|
||||
Url({required this.url});
|
||||
|
||||
factory Url.fromJson(Map<String, dynamic> json) => _$UrlFromJson(json);
|
||||
Map<String, dynamic> 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<String>? 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<String, dynamic> json) => _$RouteFromJson(json);
|
||||
Map<String, dynamic> 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<String, dynamic> json) =>
|
||||
_$TunnelInfoFromJson(json);
|
||||
Map<String, dynamic> 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<String, dynamic> json) =>
|
||||
_$PeerConnStatsFromJson(json);
|
||||
Map<String, dynamic> 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<String> 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<String, dynamic> json) =>
|
||||
_$PeerConnInfoFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PeerConnInfoToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class PeerInfo {
|
||||
@JsonKey(name: 'peer_id')
|
||||
final int peerId;
|
||||
final List<PeerConnInfo> conns;
|
||||
|
||||
PeerInfo({
|
||||
required this.peerId,
|
||||
required this.conns,
|
||||
});
|
||||
|
||||
factory PeerInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$PeerInfoFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PeerInfoToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class PeerRoutePair {
|
||||
final Route? route;
|
||||
final PeerInfo? peer;
|
||||
|
||||
PeerRoutePair({
|
||||
this.route,
|
||||
this.peer,
|
||||
});
|
||||
|
||||
factory PeerRoutePair.fromJson(Map<String, dynamic> json) =>
|
||||
_$PeerRoutePairFromJson(json);
|
||||
Map<String, dynamic> 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<String, dynamic> json) =>
|
||||
_$Ipv6AddrFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$Ipv6AddrToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class GetIpListResponse {
|
||||
@JsonKey(name: 'public_ipv4')
|
||||
final Ipv4Addr? publicIpv4;
|
||||
@JsonKey(name: 'interface_ipv4s')
|
||||
final List<Ipv4Addr> interfaceIpv4s;
|
||||
@JsonKey(name: 'public_ipv6')
|
||||
final Ipv6Addr? publicIpv6;
|
||||
@JsonKey(name: 'interface_ipv6s')
|
||||
final List<Ipv6Addr> interfaceIpv6s;
|
||||
final List<Url> listeners;
|
||||
|
||||
GetIpListResponse({
|
||||
this.publicIpv4,
|
||||
required this.interfaceIpv4s,
|
||||
this.publicIpv6,
|
||||
required this.interfaceIpv6s,
|
||||
required this.listeners,
|
||||
});
|
||||
|
||||
factory GetIpListResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$GetIpListResponseFromJson(json);
|
||||
Map<String, dynamic> 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<Url> 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<String, dynamic> json) =>
|
||||
_$MyNodeInfoFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$MyNodeInfoToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Runin {
|
||||
@JsonKey(name: 'dev_name')
|
||||
final String? devName;
|
||||
@JsonKey(name: 'my_node_info')
|
||||
final MyNodeInfo? myNodeInfo;
|
||||
final List<String> events;
|
||||
final List<Route> routes;
|
||||
final List<PeerInfo>? peers;
|
||||
@JsonKey(name: 'peer_route_pairs')
|
||||
final List<PeerRoutePair>? 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<String, dynamic> json) => _$RuninFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$RuninToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'runin.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Ipv4Addr _$Ipv4AddrFromJson(Map<String, dynamic> json) => Ipv4Addr(
|
||||
addr: (json['addr'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$Ipv4AddrToJson(Ipv4Addr instance) => <String, dynamic>{
|
||||
'addr': instance.addr,
|
||||
};
|
||||
|
||||
Ipv4Inet _$Ipv4InetFromJson(Map<String, dynamic> json) => Ipv4Inet(
|
||||
address: json['address'] == null
|
||||
? null
|
||||
: Ipv4Addr.fromJson(json['address'] as Map<String, dynamic>),
|
||||
networkLength: (json['network_length'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$Ipv4InetToJson(Ipv4Inet instance) => <String, dynamic>{
|
||||
'address': instance.address,
|
||||
'network_length': instance.networkLength,
|
||||
};
|
||||
|
||||
StunInfo _$StunInfoFromJson(Map<String, dynamic> 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<dynamic>).map((e) => e as String).toList(),
|
||||
minPort: (json['min_port'] as num).toInt(),
|
||||
maxPort: (json['max_port'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$StunInfoToJson(StunInfo instance) => <String, dynamic>{
|
||||
'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<String, dynamic> 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<String, dynamic> _$PeerFeatureFlagToJson(PeerFeatureFlag instance) =>
|
||||
<String, dynamic>{
|
||||
'is_public_server': instance.isPublicServer,
|
||||
'avoid_relay_data': instance.avoidRelayData,
|
||||
'kcp_input': instance.kcpInput,
|
||||
'no_relay_kcp': instance.noRelayKcp,
|
||||
};
|
||||
|
||||
Url _$UrlFromJson(Map<String, dynamic> json) => Url(
|
||||
url: json['url'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UrlToJson(Url instance) => <String, dynamic>{
|
||||
'url': instance.url,
|
||||
};
|
||||
|
||||
Route _$RouteFromJson(Map<String, dynamic> json) => Route(
|
||||
peerId: (json['peer_id'] as num?)?.toInt(),
|
||||
ipv4Addr: json['ipv4_addr'] == null
|
||||
? null
|
||||
: Ipv4Inet.fromJson(json['ipv4_addr'] as Map<String, dynamic>),
|
||||
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<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
hostname: json['hostname'] as String?,
|
||||
stunInfo: json['stun_info'] == null
|
||||
? null
|
||||
: StunInfo.fromJson(json['stun_info'] as Map<String, dynamic>),
|
||||
instId: json['inst_id'] as String?,
|
||||
version: json['version'] as String?,
|
||||
featureFlag: json['feature_flag'] == null
|
||||
? null
|
||||
: PeerFeatureFlag.fromJson(
|
||||
json['feature_flag'] as Map<String, dynamic>),
|
||||
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<String, dynamic> _$RouteToJson(Route instance) => <String, dynamic>{
|
||||
'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<String, dynamic> json) => TunnelInfo(
|
||||
tunnelType: json['tunnel_type'] as String,
|
||||
localAddr: json['local_addr'] == null
|
||||
? null
|
||||
: Url.fromJson(json['local_addr'] as Map<String, dynamic>),
|
||||
remoteAddr: json['remote_addr'] == null
|
||||
? null
|
||||
: Url.fromJson(json['remote_addr'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TunnelInfoToJson(TunnelInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'tunnel_type': instance.tunnelType,
|
||||
'local_addr': instance.localAddr,
|
||||
'remote_addr': instance.remoteAddr,
|
||||
};
|
||||
|
||||
PeerConnStats _$PeerConnStatsFromJson(Map<String, dynamic> 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<String, dynamic> _$PeerConnStatsToJson(PeerConnStats instance) =>
|
||||
<String, dynamic>{
|
||||
'rx_bytes': instance.rxBytes,
|
||||
'tx_bytes': instance.txBytes,
|
||||
'rx_packets': instance.rxPackets,
|
||||
'tx_packets': instance.txPackets,
|
||||
'latency_us': instance.latencyUs,
|
||||
};
|
||||
|
||||
PeerConnInfo _$PeerConnInfoFromJson(Map<String, dynamic> 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<dynamic>).map((e) => e as String).toList(),
|
||||
tunnel: json['tunnel'] == null
|
||||
? null
|
||||
: TunnelInfo.fromJson(json['tunnel'] as Map<String, dynamic>),
|
||||
stats: json['stats'] == null
|
||||
? null
|
||||
: PeerConnStats.fromJson(json['stats'] as Map<String, dynamic>),
|
||||
lossRate: (json['loss_rate'] as num).toDouble(),
|
||||
isClient: json['is_client'] as bool,
|
||||
networkName: json['network_name'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PeerConnInfoToJson(PeerConnInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'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<String, dynamic> json) => PeerInfo(
|
||||
peerId: (json['peer_id'] as num).toInt(),
|
||||
conns: (json['conns'] as List<dynamic>)
|
||||
.map((e) => PeerConnInfo.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PeerInfoToJson(PeerInfo instance) => <String, dynamic>{
|
||||
'peer_id': instance.peerId,
|
||||
'conns': instance.conns,
|
||||
};
|
||||
|
||||
PeerRoutePair _$PeerRoutePairFromJson(Map<String, dynamic> json) =>
|
||||
PeerRoutePair(
|
||||
route: json['route'] == null
|
||||
? null
|
||||
: Route.fromJson(json['route'] as Map<String, dynamic>),
|
||||
peer: json['peer'] == null
|
||||
? null
|
||||
: PeerInfo.fromJson(json['peer'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PeerRoutePairToJson(PeerRoutePair instance) =>
|
||||
<String, dynamic>{
|
||||
'route': instance.route,
|
||||
'peer': instance.peer,
|
||||
};
|
||||
|
||||
Ipv6Addr _$Ipv6AddrFromJson(Map<String, dynamic> 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<String, dynamic> _$Ipv6AddrToJson(Ipv6Addr instance) => <String, dynamic>{
|
||||
'part1': instance.part1,
|
||||
'part2': instance.part2,
|
||||
'part3': instance.part3,
|
||||
'part4': instance.part4,
|
||||
};
|
||||
|
||||
GetIpListResponse _$GetIpListResponseFromJson(Map<String, dynamic> json) =>
|
||||
GetIpListResponse(
|
||||
publicIpv4: json['public_ipv4'] == null
|
||||
? null
|
||||
: Ipv4Addr.fromJson(json['public_ipv4'] as Map<String, dynamic>),
|
||||
interfaceIpv4s: (json['interface_ipv4s'] as List<dynamic>)
|
||||
.map((e) => Ipv4Addr.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
publicIpv6: json['public_ipv6'] == null
|
||||
? null
|
||||
: Ipv6Addr.fromJson(json['public_ipv6'] as Map<String, dynamic>),
|
||||
interfaceIpv6s: (json['interface_ipv6s'] as List<dynamic>)
|
||||
.map((e) => Ipv6Addr.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
listeners: (json['listeners'] as List<dynamic>)
|
||||
.map((e) => Url.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$GetIpListResponseToJson(GetIpListResponse instance) =>
|
||||
<String, dynamic>{
|
||||
'public_ipv4': instance.publicIpv4,
|
||||
'interface_ipv4s': instance.interfaceIpv4s,
|
||||
'public_ipv6': instance.publicIpv6,
|
||||
'interface_ipv6s': instance.interfaceIpv6s,
|
||||
'listeners': instance.listeners,
|
||||
};
|
||||
|
||||
MyNodeInfo _$MyNodeInfoFromJson(Map<String, dynamic> json) => MyNodeInfo(
|
||||
virtualIpv4: json['virtual_ipv4'] == null
|
||||
? null
|
||||
: Ipv4Inet.fromJson(json['virtual_ipv4'] as Map<String, dynamic>),
|
||||
hostname: json['hostname'] as String,
|
||||
version: json['version'] as String,
|
||||
ips: json['ips'] == null
|
||||
? null
|
||||
: GetIpListResponse.fromJson(json['ips'] as Map<String, dynamic>),
|
||||
stunInfo: json['stun_info'] == null
|
||||
? null
|
||||
: StunInfo.fromJson(json['stun_info'] as Map<String, dynamic>),
|
||||
listeners: (json['listeners'] as List<dynamic>)
|
||||
.map((e) => Url.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
vpnPortalCfg: json['vpn_portal_cfg'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MyNodeInfoToJson(MyNodeInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'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<String, dynamic> json) => Runin(
|
||||
devName: json['dev_name'] as String?,
|
||||
myNodeInfo: json['my_node_info'] == null
|
||||
? null
|
||||
: MyNodeInfo.fromJson(json['my_node_info'] as Map<String, dynamic>),
|
||||
events:
|
||||
(json['events'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
routes: (json['routes'] as List<dynamic>)
|
||||
.map((e) => Route.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
peers: (json['peers'] as List<dynamic>?)
|
||||
?.map((e) => PeerInfo.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
peerRoutePairs: (json['peer_route_pairs'] as List<dynamic>?)
|
||||
?.map((e) => PeerRoutePair.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
running: json['running'] as bool,
|
||||
errorMsg: json['error_msg'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RuninToJson(Runin instance) => <String, dynamic>{
|
||||
'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,
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
String getThemeModeText(ThemeMode mode) {
|
||||
return switch (mode) {
|
||||
ThemeMode.light => '浅色模式',
|
||||
ThemeMode.dark => '深色模式',
|
||||
ThemeMode.system => '跟随系统',
|
||||
};
|
||||
}
|
||||
@@ -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<KVNodeInfo> _nodes = [];
|
||||
List<KVNodeInfo> get nodes => _nodes;
|
||||
set nodes(List<KVNodeInfo> value) {
|
||||
_nodes = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -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: <Widget>[
|
||||
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: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('取消'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('确定'),
|
||||
onPressed: () {
|
||||
// 使用 Future.microtask 延迟颜色更改,避免在当前帧中触发重建
|
||||
Navigator.of(context).pop();
|
||||
Future.microtask(() {
|
||||
changeSeedColor(pickerColor);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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<FloatingCard> createState() => _FloatingCardState();
|
||||
}
|
||||
|
||||
class _FloatingCardState extends State<FloatingCard> {
|
||||
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,
|
||||
)),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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: '关闭',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+997
@@ -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"
|
||||
+116
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+5111
File diff suppressed because it is too large
Load Diff
@@ -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)'] }
|
||||
Generated
+5274
File diff suppressed because it is too large
Load Diff
@@ -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"]
|
||||
@@ -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<PathBuf> {
|
||||
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<std::path::PathBuf, Box<dyn std::error::Error>> {
|
||||
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<String> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<dyn std::error::Error>> {
|
||||
#[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(())
|
||||
}
|
||||
@@ -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://0.0.0.0:11010>. tcp can be tcp, udp, ring, wg, ws, wss\n
|
||||
proto & port pair: <proto:port>. 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://0.0.0.0:11010>,其中tcp可以是tcp、udp、ring、wg、ws、wss协议。
|
||||
协议和端口对:<proto:port>,例如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: <port>, 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"
|
||||
@@ -0,0 +1,2 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
@@ -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<S: AsRawSocket>(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<usize> {
|
||||
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<u32> {
|
||||
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<S: AsRawSocket>(
|
||||
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<S: AsRawSocket>(
|
||||
socket: &S,
|
||||
bind_addr: &SocketAddr,
|
||||
bind_dev: Option<String>,
|
||||
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<Self> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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<Vec<u8>, 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<Vec<u8>, 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String>);
|
||||
|
||||
fn get_inst_name(&self) -> String;
|
||||
fn set_inst_name(&self, name: String);
|
||||
|
||||
fn get_netns(&self) -> Option<String>;
|
||||
fn set_netns(&self, ns: Option<String>);
|
||||
|
||||
fn get_ipv4(&self) -> Option<cidr::Ipv4Inet>;
|
||||
fn set_ipv4(&self, addr: Option<cidr::Ipv4Inet>);
|
||||
|
||||
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<cidr::IpCidr>;
|
||||
|
||||
fn get_network_identity(&self) -> NetworkIdentity;
|
||||
fn set_network_identity(&self, identity: NetworkIdentity);
|
||||
|
||||
fn get_listener_uris(&self) -> Vec<url::Url>;
|
||||
|
||||
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<PeerConfig>;
|
||||
fn set_peers(&self, peers: Vec<PeerConfig>);
|
||||
|
||||
fn get_listeners(&self) -> Vec<url::Url>;
|
||||
fn set_listeners(&self, listeners: Vec<url::Url>);
|
||||
|
||||
fn get_mapped_listeners(&self) -> Vec<url::Url>;
|
||||
fn set_mapped_listeners(&self, listeners: Option<Vec<url::Url>>);
|
||||
|
||||
fn get_rpc_portal(&self) -> Option<SocketAddr>;
|
||||
fn set_rpc_portal(&self, addr: SocketAddr);
|
||||
|
||||
fn get_vpn_portal_config(&self) -> Option<VpnPortalConfig>;
|
||||
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<Ipv4Addr>;
|
||||
fn set_exit_nodes(&self, nodes: Vec<Ipv4Addr>);
|
||||
|
||||
fn get_routes(&self) -> Option<Vec<cidr::Ipv4Cidr>>;
|
||||
fn set_routes(&self, routes: Option<Vec<cidr::Ipv4Cidr>>);
|
||||
|
||||
fn get_socks5_portal(&self) -> Option<url::Url>;
|
||||
fn set_socks5_portal(&self, addr: Option<url::Url>);
|
||||
|
||||
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<String>,
|
||||
#[serde(skip)]
|
||||
pub network_secret_digest: Option<NetworkSecretDigest>,
|
||||
}
|
||||
|
||||
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<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
|
||||
pub struct FileLoggerConfig {
|
||||
pub level: Option<String>,
|
||||
pub file: Option<String>,
|
||||
pub dir: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
|
||||
pub struct ConsoleLoggerConfig {
|
||||
pub level: Option<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
hostname: Option<String>,
|
||||
instance_name: Option<String>,
|
||||
instance_id: Option<uuid::Uuid>,
|
||||
ipv4: Option<String>,
|
||||
dhcp: Option<bool>,
|
||||
network_identity: Option<NetworkIdentity>,
|
||||
listeners: Option<Vec<url::Url>>,
|
||||
mapped_listeners: Option<Vec<url::Url>>,
|
||||
exit_nodes: Option<Vec<Ipv4Addr>>,
|
||||
|
||||
peer: Option<Vec<PeerConfig>>,
|
||||
proxy_network: Option<Vec<ProxyNetworkConfig>>,
|
||||
|
||||
file_logger: Option<FileLoggerConfig>,
|
||||
console_logger: Option<ConsoleLoggerConfig>,
|
||||
|
||||
rpc_portal: Option<SocketAddr>,
|
||||
|
||||
vpn_portal_config: Option<VpnPortalConfig>,
|
||||
|
||||
routes: Option<Vec<cidr::Ipv4Cidr>>,
|
||||
|
||||
socks5_proxy: Option<url::Url>,
|
||||
|
||||
flags: Option<serde_json::Map<String, serde_json::Value>>,
|
||||
|
||||
#[serde(skip)]
|
||||
flags_struct: Option<Flags>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TomlConfigLoader {
|
||||
config: Arc<Mutex<Config>>,
|
||||
}
|
||||
|
||||
impl Default for TomlConfigLoader {
|
||||
fn default() -> Self {
|
||||
TomlConfigLoader::new_from_str("").unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl TomlConfigLoader {
|
||||
pub fn new_from_str(config_str: &str) -> Result<Self, anyhow::Error> {
|
||||
let mut config = toml::de::from_str::<Config>(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<Self, anyhow::Error> {
|
||||
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<String, serde_json::Value>) -> Flags {
|
||||
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
|
||||
let default_flags_hashmap =
|
||||
serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&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::<String>();
|
||||
|
||||
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<String>) {
|
||||
self.config.lock().unwrap().hostname = name;
|
||||
}
|
||||
|
||||
fn get_netns(&self) -> Option<String> {
|
||||
self.config.lock().unwrap().netns.clone()
|
||||
}
|
||||
|
||||
fn set_netns(&self, ns: Option<String>) {
|
||||
self.config.lock().unwrap().netns = ns;
|
||||
}
|
||||
|
||||
fn get_ipv4(&self) -> Option<cidr::Ipv4Inet> {
|
||||
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<cidr::Ipv4Inet>) {
|
||||
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<cidr::IpCidr> {
|
||||
self.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.proxy_network
|
||||
.as_ref()
|
||||
.map(|v| {
|
||||
v.iter()
|
||||
.map(|c| c.cidr.parse().unwrap())
|
||||
.collect::<Vec<cidr::IpCidr>>()
|
||||
})
|
||||
.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<url::Url> {
|
||||
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<PeerConfig> {
|
||||
self.config.lock().unwrap().peer.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn set_peers(&self, peers: Vec<PeerConfig>) {
|
||||
self.config.lock().unwrap().peer = Some(peers);
|
||||
}
|
||||
|
||||
fn get_listeners(&self) -> Vec<url::Url> {
|
||||
self.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.listeners
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn set_listeners(&self, listeners: Vec<url::Url>) {
|
||||
self.config.lock().unwrap().listeners = Some(listeners);
|
||||
}
|
||||
|
||||
fn get_mapped_listeners(&self) -> Vec<url::Url> {
|
||||
self.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.mapped_listeners
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn set_mapped_listeners(&self, listeners: Option<Vec<url::Url>>) {
|
||||
self.config.lock().unwrap().mapped_listeners = listeners;
|
||||
}
|
||||
|
||||
fn get_rpc_portal(&self) -> Option<SocketAddr> {
|
||||
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<VpnPortalConfig> {
|
||||
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<Ipv4Addr> {
|
||||
self.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.exit_nodes
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn set_exit_nodes(&self, nodes: Vec<Ipv4Addr>) {
|
||||
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::<serde_json::Map<String, serde_json::Value>>(&default_flags_json)
|
||||
.unwrap();
|
||||
|
||||
let cur_flags_json = serde_json::to_string(&self.get_flags()).unwrap();
|
||||
let cur_flags_hashmap =
|
||||
serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&cur_flags_json)
|
||||
.unwrap();
|
||||
|
||||
let mut flag_map: serde_json::Map<String, serde_json::Value> = 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<Vec<cidr::Ipv4Cidr>> {
|
||||
self.config.lock().unwrap().routes.clone()
|
||||
}
|
||||
|
||||
fn set_routes(&self, routes: Option<Vec<cidr::Ipv4Cidr>>) {
|
||||
self.config.lock().unwrap().routes = routes;
|
||||
}
|
||||
|
||||
fn get_socks5_portal(&self) -> Option<url::Url> {
|
||||
self.config.lock().unwrap().socks5_proxy.clone()
|
||||
}
|
||||
|
||||
fn set_socks5_portal(&self, addr: Option<url::Url>) {
|
||||
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::<Vec<String>>()
|
||||
);
|
||||
|
||||
println!("{}", ret.dump());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
macro_rules! define_global_var {
|
||||
($name:ident, $type:ty, $init:expr) => {
|
||||
pub static $name: once_cell::sync::Lazy<tokio::sync::Mutex<$type>> =
|
||||
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")
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
#[doc(hidden)]
|
||||
pub struct Defer<F: FnOnce()> {
|
||||
// internal struct used by defer! macro
|
||||
func: Option<F>,
|
||||
}
|
||||
|
||||
impl<F: FnOnce()> Defer<F> {
|
||||
pub fn new(func: F) -> Self {
|
||||
Self { func: Some(func) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FnOnce()> Drop for Defer<F> {
|
||||
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)* });
|
||||
};
|
||||
}
|
||||
@@ -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<String>),
|
||||
#[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<T> = result::Result<T, Error>;
|
||||
|
||||
// impl From for std::
|
||||
@@ -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<cidr::Ipv4Inet>, Option<cidr::Ipv4Inet>), // (old, new)
|
||||
DhcpIpv4Conflicted(Option<cidr::Ipv4Inet>),
|
||||
}
|
||||
|
||||
pub type EventBus = tokio::sync::broadcast::Sender<GlobalCtxEvent>;
|
||||
pub type EventBusSubscriber = tokio::sync::broadcast::Receiver<GlobalCtxEvent>;
|
||||
|
||||
pub struct GlobalCtx {
|
||||
pub inst_name: String,
|
||||
pub id: uuid::Uuid,
|
||||
pub config: Box<dyn ConfigLoader>,
|
||||
pub net_ns: NetNS,
|
||||
pub network: NetworkIdentity,
|
||||
|
||||
event_bus: EventBus,
|
||||
|
||||
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
|
||||
cached_proxy_cidrs: AtomicCell<Option<Vec<cidr::IpCidr>>>,
|
||||
|
||||
ip_collector: Arc<IPCollector>,
|
||||
|
||||
hostname: String,
|
||||
|
||||
stun_info_collection: Box<dyn StunInfoCollectorTrait>,
|
||||
|
||||
running_listeners: Mutex<Vec<url::Url>>,
|
||||
|
||||
enable_exit_node: bool,
|
||||
proxy_forward_by_system: bool,
|
||||
no_tun: bool,
|
||||
|
||||
feature_flags: AtomicCell<PeerFeatureFlag>,
|
||||
}
|
||||
|
||||
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<GlobalCtx>;
|
||||
|
||||
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<cidr::Ipv4Inet> {
|
||||
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<cidr::Ipv4Inet>) {
|
||||
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<cidr::IpCidr> {
|
||||
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<IPCollector> {
|
||||
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<dyn StunInfoCollectorTrait>) {
|
||||
// force replace the stun_info_collection without mut and drop the old one
|
||||
let ptr = &self.stun_info_collection as *const Box<dyn StunInfoCollectorTrait>;
|
||||
let ptr = ptr as *mut Box<dyn StunInfoCollectorTrait>;
|
||||
unsafe {
|
||||
std::ptr::drop_in_place(ptr);
|
||||
#[allow(invalid_reference_casting)]
|
||||
std::ptr::write(ptr, collector);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_running_listeners(&self) -> Vec<url::Url> {
|
||||
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<cidr::Ipv4Cidr> {
|
||||
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<NetworkIdentity>,
|
||||
) -> 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)
|
||||
}
|
||||
}
|
||||
@@ -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<Ipv4Addr>) -> 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
|
||||
}
|
||||
}
|
||||
@@ -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<Ipv4Addr>) -> 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;
|
||||
@@ -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<std::net::UdpSocket, Error> {
|
||||
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<T: NetlinkDeserializable + NetlinkSerializable + Debug>(
|
||||
req: T,
|
||||
flags: u16,
|
||||
) -> Result<Socket, Error> {
|
||||
let mut socket = Socket::new(NETLINK_ROUTE)?;
|
||||
socket.bind_auto()?;
|
||||
socket.connect(&SocketAddr::new(0, 0))?;
|
||||
|
||||
let mut req: NetlinkMessage<T> =
|
||||
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<T: NetlinkDeserializable + NetlinkSerializable + Debug>(
|
||||
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::<T>::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<IpAddr> {
|
||||
match addr {
|
||||
RouteAddress::Inet(addr) => Some(addr.into()),
|
||||
RouteAddress::Inet6(addr) => Some(addr.into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RouteMessage> 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<u32, Error> {
|
||||
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<u8, Error> {
|
||||
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>(
|
||||
RouteNetlinkMessage::DelAddress(message),
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn mtu_op<T: TryInto<Ioctl>>(
|
||||
name: &str,
|
||||
op: T,
|
||||
value: libc::c_int,
|
||||
) -> Result<u32, Error>
|
||||
where
|
||||
<T as TryInto<Ioctl>>::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<u32, Error> {
|
||||
Self::mtu_op(name, SIOCGIFMTU, 0)
|
||||
}
|
||||
|
||||
pub fn list_addresses(name: &str) -> Result<Vec<IpInet>, 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<T: TryInto<Ioctl>>(
|
||||
name: &str,
|
||||
op: T,
|
||||
flags: InterfaceFlags,
|
||||
) -> Result<InterfaceFlags, Error>
|
||||
where
|
||||
<T as TryInto<Ioctl>>::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<InterfaceFlags, Error> {
|
||||
Self::set_flags_op(name, SIOCSIFFLAGS, flags)
|
||||
}
|
||||
|
||||
pub(crate) fn get_flags(name: &str) -> Result<InterfaceFlags, Error> {
|
||||
Self::set_flags_op(name, SIOCGIFFLAGS, InterfaceFlags::empty())
|
||||
}
|
||||
|
||||
fn list_routes() -> Result<Vec<RouteMessage>, 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::<u8>::new();
|
||||
loop {
|
||||
if resp.len() == 0 {
|
||||
let (new_resp, _) = s.recv_from_full()?;
|
||||
resp = new_resp;
|
||||
}
|
||||
let ret = NetlinkMessage::<RouteNetlinkMessage>::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>(
|
||||
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<Ipv4Addr>) -> 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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
assert!(!routes.contains(&IpAddr::V4("10.5.5.0".parse().unwrap())));
|
||||
}
|
||||
}
|
||||
@@ -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<IpAddr>,
|
||||
|
||||
/// 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<u32>,
|
||||
|
||||
#[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<IpAddr>,
|
||||
|
||||
/// 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<IpAddr>,
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
/// The route metric offset value for this route.
|
||||
pub metric: Option<u32>,
|
||||
|
||||
#[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<u64>,
|
||||
}
|
||||
|
||||
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),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<u32> {
|
||||
crate::arch::windows::find_interface_index(name).ok()
|
||||
}
|
||||
|
||||
async fn list_ipv4(name: &str) -> Result<Vec<Ipv4Addr>, 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::<Vec<_>>();
|
||||
|
||||
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<Ipv4Addr>) -> 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
|
||||
}
|
||||
}
|
||||
@@ -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<F: time::formatting::Formattable>(
|
||||
format: F,
|
||||
) -> tracing_subscriber::fmt::time::OffsetTime<F> {
|
||||
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<time::format_description::well_known::Rfc3339> {
|
||||
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<T: Debug + Send + Sync + 'static>(
|
||||
js: Arc<Mutex<JoinSet<T>>>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<std::fs::File>,
|
||||
}
|
||||
|
||||
pub static ROOT_NETNS_NAME: &str = "_root_ns";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl NetNSGuard {
|
||||
pub fn new(ns: Option<String>) -> Box<Self> {
|
||||
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<String>) {
|
||||
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<String>) -> Box<Self> {
|
||||
Box::new(NetNSGuard {})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NetNS {
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
impl NetNS {
|
||||
pub fn new(name: Option<String>) -> Self {
|
||||
NetNS { name }
|
||||
}
|
||||
|
||||
pub async fn run_async<F, Fut, Ret>(&self, f: F) -> Ret
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = Ret>,
|
||||
{
|
||||
// TODO: do we really need this lock
|
||||
// let _lock = LOCK.lock().await;
|
||||
let _guard = NetNSGuard::new(self.name.clone());
|
||||
f().await
|
||||
}
|
||||
|
||||
pub fn run<F, Ret>(&self, f: F) -> Ret
|
||||
where
|
||||
F: FnOnce() -> Ret,
|
||||
{
|
||||
let _guard = NetNSGuard::new(self.name.clone());
|
||||
f()
|
||||
}
|
||||
|
||||
pub fn guard(&self) -> Box<NetNSGuard> {
|
||||
NetNSGuard::new(self.name.clone())
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<String> {
|
||||
self.name.clone()
|
||||
}
|
||||
}
|
||||
@@ -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<std::net::Ipv4Addr> {
|
||||
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<std::net::Ipv6Addr> {
|
||||
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<RwLock<GetIpListResponse>>,
|
||||
collect_ip_task: Mutex<JoinSet<()>>,
|
||||
net_ns: NetNS,
|
||||
stun_info_collector: Arc<Box<dyn StunInfoCollectorTrait>>,
|
||||
}
|
||||
|
||||
impl IPCollector {
|
||||
pub fn new<T: StunInfoCollectorTrait + 'static>(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::<IpAddr>() 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<NetworkInterface> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<T> {
|
||||
inner: JoinHandle<T>,
|
||||
}
|
||||
|
||||
impl<T> Drop for ScopedTask<T> {
|
||||
fn drop(&mut self) {
|
||||
self.inner.abort()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for ScopedTask<T> {
|
||||
type Output = <JoinHandle<T> as Future>::Output;
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.inner).poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<JoinHandle<T>> for ScopedTask<T> {
|
||||
fn from(inner: JoinHandle<T>) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for ScopedTask<T> {
|
||||
type Target = JoinHandle<T>;
|
||||
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<RwLock<bool>>);
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
ips: Vec<SocketAddr>,
|
||||
max_ip_per_domain: u32,
|
||||
}
|
||||
|
||||
impl HostResolverIter {
|
||||
fn new(hostnames: Vec<String>, max_ip_per_domain: u32) -> Self {
|
||||
Self {
|
||||
hostnames,
|
||||
ips: vec![],
|
||||
max_ip_per_domain,
|
||||
}
|
||||
}
|
||||
|
||||
#[async_recursion::async_recursion]
|
||||
async fn next(&mut self) -> Option<SocketAddr> {
|
||||
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<u8>,
|
||||
addr: SocketAddr,
|
||||
}
|
||||
|
||||
type StunPacketReceiver = tokio::sync::broadcast::Receiver<StunPacket>;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct BindRequestResponse {
|
||||
local_addr: SocketAddr,
|
||||
stun_server_addr: SocketAddr,
|
||||
|
||||
recv_from_addr: SocketAddr,
|
||||
mapped_socket_addr: Option<SocketAddr>,
|
||||
changed_socket_addr: Option<SocketAddr>,
|
||||
|
||||
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<UdpSocket>,
|
||||
stun_packet_receiver: Arc<Mutex<StunPacketReceiver>>,
|
||||
}
|
||||
|
||||
impl StunClient {
|
||||
pub fn new(
|
||||
stun_server: SocketAddr,
|
||||
socket: Arc<UdpSocket>,
|
||||
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<u32>,
|
||||
expected_ip_changed: bool,
|
||||
expected_port_changed: bool,
|
||||
stun_host: &SocketAddr,
|
||||
) -> Result<(Message<Attribute>, 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::<Attribute>::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<Attribute>) -> Option<SocketAddr> {
|
||||
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<Attribute>) -> Option<SocketAddr> {
|
||||
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<BindRequestResponse, Error> {
|
||||
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::<u32>();
|
||||
// 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::<Attribute>::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<UdpSocket>,
|
||||
task_set: JoinSet<()>,
|
||||
stun_packet_sender: broadcast::Sender<StunPacket>,
|
||||
}
|
||||
|
||||
impl StunClientBuilder {
|
||||
pub fn new(udp: Arc<UdpSocket>) -> 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<BindRequestResponse>,
|
||||
// if we are easy symmetric nat, we need to test with another port to check inc or dec
|
||||
extra_bind_test: Option<BindRequestResponse>,
|
||||
}
|
||||
|
||||
impl UdpNatTypeDetectResult {
|
||||
fn new(source_addr: SocketAddr, stun_resps: Vec<BindRequestResponse>) -> 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::<BTreeSet<_>>()
|
||||
.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::<BTreeSet<_>>()
|
||||
.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<IpAddr> {
|
||||
self.stun_resps
|
||||
.iter()
|
||||
.filter_map(|x| x.mapped_socket_addr.map(|x| x.ip()))
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn collect_available_stun_server(&self) -> Vec<SocketAddr> {
|
||||
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<String>,
|
||||
max_ip_per_domain: u32,
|
||||
}
|
||||
|
||||
impl UdpNatTypeDetector {
|
||||
pub fn new(stun_server_hosts: Vec<String>, 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<BindRequestResponse, Error> {
|
||||
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<UdpNatTypeDetectResult, Error> {
|
||||
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<UdpSocket>,
|
||||
) -> Result<UdpNatTypeDetectResult, Error> {
|
||||
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<SocketAddr, Error>;
|
||||
}
|
||||
|
||||
pub struct StunInfoCollector {
|
||||
stun_servers: Arc<RwLock<Vec<String>>>,
|
||||
udp_nat_test_result: Arc<RwLock<Option<UdpNatTypeDetectResult>>>,
|
||||
nat_test_result_time: Arc<AtomicCell<chrono::DateTime<Local>>>,
|
||||
redetect_notify: Arc<tokio::sync::Notify>,
|
||||
tasks: std::sync::Mutex<JoinSet<()>>,
|
||||
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<SocketAddr, Error> {
|
||||
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<String>) -> 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<String> {
|
||||
// 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<std::net::SocketAddr, Error> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<usize> {
|
||||
track!(self.0.decode(buf, eos))
|
||||
}
|
||||
|
||||
fn finish_decoding(&mut self) -> Result<Self::Item> {
|
||||
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<bool> {
|
||||
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<usize> {
|
||||
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<A: stun_codec::Attribute>(
|
||||
&mut self,
|
||||
message: &Message<A>,
|
||||
) -> bytecodec::Result<()> {
|
||||
self.0 = socket_addr_xor(self.0, message.transaction_id());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn after_decode<A: stun_codec::Attribute>(
|
||||
&mut self,
|
||||
message: &Message<A>,
|
||||
) -> 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<A: stun_codec::Attribute>(
|
||||
&mut self,
|
||||
message: &Message<A>,
|
||||
) -> bytecodec::Result<()> {
|
||||
self.0 = socket_addr_xor(self.0, message.transaction_id());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn after_decode<A: stun_codec::Attribute>(
|
||||
&mut self,
|
||||
message: &Message<A>,
|
||||
) -> 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
|
||||
]
|
||||
);
|
||||
@@ -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<PeerId>;
|
||||
async fn list_peer_conns(&self, peer_id: PeerId) -> Option<Vec<PeerConnInfo>>;
|
||||
fn get_peer_rpc_mgr(&self) -> Arc<PeerRpcManager>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl PeerManagerForDirectConnector for PeerManager {
|
||||
async fn list_peers(&self) -> Vec<PeerId> {
|
||||
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<Vec<PeerConnInfo>> {
|
||||
self.get_peer_map().list_peer_conns(peer_id).await
|
||||
}
|
||||
|
||||
fn get_peer_rpc_mgr(&self) -> Arc<PeerRpcManager> {
|
||||
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<PeerManager>,
|
||||
dst_blacklist: timedmap::TimedMap<DstBlackListItem, ()>,
|
||||
dst_listener_blacklist: timedmap::TimedMap<DstListenerUrlBlackListItem, ()>,
|
||||
}
|
||||
|
||||
impl DirectConnectorManagerData {
|
||||
pub fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc<PeerManager>) -> 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<DirectConnectorManagerData>,
|
||||
|
||||
tasks: JoinSet<()>,
|
||||
}
|
||||
|
||||
impl DirectConnectorManager {
|
||||
pub fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc<PeerManager>) -> 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<DirectConnectorManagerData>,
|
||||
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<DirectConnectorManagerData>,
|
||||
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<DirectConnectorManagerData>,
|
||||
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::<url::Url>::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::<Vec<_>>();
|
||||
|
||||
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<DirectConnectorManagerData>,
|
||||
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::<DirectConnectorRpcClientFactory<BaseController>>(
|
||||
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::<std::net::Ipv4Addr>().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())));
|
||||
}
|
||||
}
|
||||
@@ -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<T>(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<SocketAddr>,
|
||||
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<Box<dyn TunnelConnector>, 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::<Vec<_>>();
|
||||
|
||||
// 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<Box<dyn TunnelConnector>, 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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
let _ = futures::future::join_all(srv_lookup_tasks).await;
|
||||
|
||||
let srv_records = responses.iter().map(|r| r.clone()).collect::<Vec<_>>();
|
||||
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<Box<dyn Tunnel>, 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<SocketAddr>) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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<dyn Tunnel>,
|
||||
info: TunnelInfo,
|
||||
}
|
||||
|
||||
impl TunnelWithInfo {
|
||||
pub fn new(inner: Box<dyn Tunnel>, info: TunnelInfo) -> Self {
|
||||
Self { inner, info }
|
||||
}
|
||||
}
|
||||
|
||||
impl Tunnel for TunnelWithInfo {
|
||||
fn split(&self) -> (Pin<Box<dyn ZCPacketStream>>, Pin<Box<dyn ZCPacketSink>>) {
|
||||
self.inner.split()
|
||||
}
|
||||
|
||||
fn info(&self) -> Option<TunnelInfo> {
|
||||
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<SocketAddr>,
|
||||
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<Box<dyn TunnelConnector>, 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::<Vec<_>>();
|
||||
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<Box<dyn TunnelConnector>, 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::<Vec<&str>>();
|
||||
|
||||
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<Box<dyn TunnelConnector>, 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<Box<dyn Tunnel>, 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<SocketAddr>) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<Mutex<Box<dyn TunnelConnector>>>;
|
||||
type ConnectorMap = Arc<DashMap<String, MutexConnector>>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ReconnResult {
|
||||
dead_url: String,
|
||||
peer_id: PeerId,
|
||||
conn_id: PeerConnId,
|
||||
}
|
||||
|
||||
struct ConnectorManagerData {
|
||||
connectors: ConnectorMap,
|
||||
reconnecting: DashSet<String>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
alive_conn_urls: Arc<DashSet<String>>,
|
||||
// user removed connector urls
|
||||
removed_conn_urls: Arc<DashSet<String>>,
|
||||
net_ns: NetNS,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
}
|
||||
|
||||
pub struct ManualConnectorManager {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
data: Arc<ConnectorManagerData>,
|
||||
tasks: JoinSet<()>,
|
||||
}
|
||||
|
||||
impl ManualConnectorManager {
|
||||
pub fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc<PeerManager>) -> 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<T>(&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<Connector> {
|
||||
let conn_urls: BTreeSet<String> = self
|
||||
.data
|
||||
.connectors
|
||||
.iter()
|
||||
.map(|x| x.key().clone().into())
|
||||
.collect();
|
||||
|
||||
let dead_urls: BTreeSet<String> = 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<String> = 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<ConnectorManagerData>,
|
||||
mut event_recv: Receiver<GlobalCtxEvent>,
|
||||
) {
|
||||
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<ConnectorManagerData>) {
|
||||
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<ConnectorManagerData>) {
|
||||
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<ConnectorManagerData>) -> BTreeSet<String> {
|
||||
Self::handle_remove_connector(data.clone());
|
||||
|
||||
let all_urls: BTreeSet<String> = 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<ConnectorManagerData>,
|
||||
dead_url: String,
|
||||
connector: MutexConnector,
|
||||
ip_version: IpVersion,
|
||||
) -> Result<ReconnResult, Error> {
|
||||
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<ConnectorManagerData>,
|
||||
dead_url: String,
|
||||
connector: MutexConnector,
|
||||
) -> Result<ReconnResult, Error> {
|
||||
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<ManualConnectorManager>);
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ConnectorManageRpc for ConnectorManagerRpcService {
|
||||
type Controller = BaseController;
|
||||
|
||||
async fn list_connector(
|
||||
&self,
|
||||
_: BaseController,
|
||||
_request: ListConnectorRequest,
|
||||
) -> Result<ListConnectorResponse, rpc_types::error::Error> {
|
||||
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<ManageConnectorResponse, rpc_types::error::Error> {
|
||||
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<Box<dyn Tunnel>, 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;
|
||||
}
|
||||
}
|
||||
@@ -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<IPCollector>,
|
||||
) {
|
||||
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<Box<dyn TunnelConnector + 'static>, 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::<SocketAddr>(&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::<SocketAddr>(&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::<uuid::Uuid>(&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::<SocketAddr>(&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::<SocketAddr>(&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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<PunchHoleServerCommon>,
|
||||
task: Mutex<Option<ScopedTask<()>>>,
|
||||
}
|
||||
|
||||
impl PunchBothEasySymHoleServer {
|
||||
pub(crate) fn new(common: Arc<PunchHoleServerCommon>) -> 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<SendPunchPacketBothEasySymResponse, rpc_types::error::Error> {
|
||||
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<PeerManager>,
|
||||
}
|
||||
|
||||
impl PunchBothEasySymHoleClient {
|
||||
pub(crate) fn new(peer_mgr: Arc<PeerManager>) -> 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<Option<Box<dyn Tunnel>>, 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::<UdpHolePunchRpcClientFactory<BaseController>>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<u16> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut port_vec: Vec<u16> = (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<NatType> 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<NatType> 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<bool> {
|
||||
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<UdpSocket>,
|
||||
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<DashMap<SocketAddr, Arc<UdpSocket>>>,
|
||||
max_socket_count: usize,
|
||||
net_ns: NetNS,
|
||||
tasks: Arc<std::sync::Mutex<JoinSet<()>>>,
|
||||
|
||||
intreast_tids: Arc<DashSet<u32>>,
|
||||
tid_to_socket: Arc<DashMap<u32, Vec<PunchedUdpSocket>>>,
|
||||
}
|
||||
|
||||
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<UdpSocket>) -> 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::<Vec<_>>();
|
||||
|
||||
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<PunchedUdpSocket> {
|
||||
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<UdpSocket>,
|
||||
tasks: JoinSet<()>,
|
||||
running: Arc<AtomicCell<bool>>,
|
||||
mapped_addr: SocketAddr,
|
||||
conn_counter: Arc<Box<dyn TunnelConnCounter>>,
|
||||
|
||||
listen_time: std::time::Instant,
|
||||
last_select_time: AtomicCell<std::time::Instant>,
|
||||
last_active_time: Arc<AtomicCell<std::time::Instant>>,
|
||||
}
|
||||
|
||||
impl UdpHolePunchListener {
|
||||
async fn get_avail_port() -> Result<u16, Error> {
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||
Ok(socket.local_addr()?.port())
|
||||
}
|
||||
|
||||
#[instrument(err)]
|
||||
pub async fn new(peer_mgr: Arc<PeerManager>) -> Result<Self, Error> {
|
||||
Self::new_ext(peer_mgr, true, None).await
|
||||
}
|
||||
|
||||
#[instrument(err)]
|
||||
pub async fn new_ext(
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
with_mapped_addr: bool,
|
||||
port: Option<u16>,
|
||||
) -> Result<Self, Error> {
|
||||
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<UdpSocket> {
|
||||
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<PeerManager>,
|
||||
|
||||
listeners: Arc<Mutex<Vec<UdpHolePunchListener>>>,
|
||||
tasks: Arc<std::sync::Mutex<JoinSet<()>>>,
|
||||
}
|
||||
|
||||
impl PunchHoleServerCommon {
|
||||
pub(crate) fn new(peer_mgr: Arc<PeerManager>) -> 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::<UdpHolePunchListener>::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<Arc<UdpSocket>> {
|
||||
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<UdpSocket>, 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<std::sync::Mutex<JoinSet<()>>> {
|
||||
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<PeerManager> {
|
||||
self.peer_mgr.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(err, ret(level=Level::DEBUG), skip(ports))]
|
||||
pub(crate) async fn send_symmetric_hole_punch_packet(
|
||||
ports: &Vec<u16>,
|
||||
udp: Arc<UdpSocket>,
|
||||
transaction_id: u32,
|
||||
public_ips: &Vec<Ipv4Addr>,
|
||||
port_start_idx: usize,
|
||||
max_packets: usize,
|
||||
) -> Result<usize, Error> {
|
||||
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<UdpSocket>,
|
||||
remote_mapped_addr: SocketAddr,
|
||||
) -> Result<Box<dyn Tunnel>, 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))
|
||||
}
|
||||
@@ -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<PunchHoleServerCommon>,
|
||||
}
|
||||
|
||||
impl PunchConeHoleServer {
|
||||
pub(crate) fn new(common: Arc<PunchHoleServerCommon>) -> Self {
|
||||
Self { common }
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self), ret, err)]
|
||||
pub(crate) async fn send_punch_packet_cone(
|
||||
&self,
|
||||
_: BaseController,
|
||||
request: SendPunchPacketConeRequest,
|
||||
) -> Result<Void, rpc_types::error::Error> {
|
||||
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<PeerManager>,
|
||||
}
|
||||
|
||||
impl PunchConeHoleClient {
|
||||
pub(crate) fn new(peer_mgr: Arc<PeerManager>) -> Self {
|
||||
Self { peer_mgr }
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub(crate) async fn do_hole_punching(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
) -> Result<Option<Box<dyn Tunnel>>, 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::<UdpHolePunchRpcClientFactory<BaseController>>(
|
||||
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<Instant> = 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);
|
||||
}
|
||||
}
|
||||
@@ -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<DashMap<PeerId, Arc<Mutex<()>>>> = Lazy::new(|| DashMap::new());
|
||||
static RUN_TESTING: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(false));
|
||||
|
||||
fn get_sym_punch_lock(peer_id: PeerId) -> Arc<Mutex<()>> {
|
||||
SYM_PUNCH_LOCK
|
||||
.entry(peer_id)
|
||||
.or_insert_with(|| Arc::new(Mutex::new(())))
|
||||
.value()
|
||||
.clone()
|
||||
}
|
||||
|
||||
struct UdpHolePunchServer {
|
||||
common: Arc<PunchHoleServerCommon>,
|
||||
cone_server: PunchConeHoleServer,
|
||||
sym_to_cone_server: PunchSymToConeHoleServer,
|
||||
both_easy_sym_server: PunchBothEasySymHoleServer,
|
||||
}
|
||||
|
||||
impl UdpHolePunchServer {
|
||||
pub fn new(peer_mgr: Arc<PeerManager>) -> Arc<Self> {
|
||||
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<SelectPunchListenerResponse> {
|
||||
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<Void> {
|
||||
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<SendPunchPacketHardSymResponse> {
|
||||
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<Void> {
|
||||
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<SendPunchPacketBothEasySymResponse> {
|
||||
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<u64>,
|
||||
current_idx: usize,
|
||||
}
|
||||
|
||||
impl BackOff {
|
||||
pub fn new(backoffs_ms: Vec<u64>) -> 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<PeerManager>,
|
||||
}
|
||||
|
||||
impl UdpHoePunchConnectorData {
|
||||
pub fn new(peer_mgr: Arc<PeerManager>) -> Arc<Self> {
|
||||
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<Option<Box<dyn Tunnel>>, 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<Self>, 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<Self>, 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<Self>, 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<UdpHoePunchConnectorData>;
|
||||
type CollectPeerItem = PunchTaskInfo;
|
||||
type TaskRet = ();
|
||||
|
||||
fn new_data(&self, peer_mgr: Arc<PeerManager>) -> Self::Data {
|
||||
UdpHoePunchConnectorData::new(peer_mgr)
|
||||
}
|
||||
|
||||
async fn collect_peers_need_task(&self, data: &Self::Data) -> Vec<Self::CollectPeerItem> {
|
||||
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<Self::CollectPeerItem> = 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<Result<Self::TaskRet, Error>> {
|
||||
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<UdpHolePunchServer>,
|
||||
client: PeerTaskManager<UdpHolePunchPeerTaskLauncher>,
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
}
|
||||
|
||||
// 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<PeerManager>) -> 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<PeerManager>, 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<PeerManager> {
|
||||
let p_a = create_mock_peer_manager().await;
|
||||
replace_stun_info_collector(p_a.clone(), udp_nat_type);
|
||||
p_a
|
||||
}
|
||||
}
|
||||
@@ -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<PunchHoleServerCommon>,
|
||||
|
||||
shuffled_port_vec: Arc<Vec<u16>>,
|
||||
}
|
||||
|
||||
impl PunchSymToConeHoleServer {
|
||||
pub(crate) fn new(common: Arc<PunchHoleServerCommon>) -> Self {
|
||||
let mut shuffled_port_vec: Vec<u16> = (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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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<SendPunchPacketHardSymResponse, rpc_types::error::Error> {
|
||||
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::<Vec<_>>();
|
||||
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<PeerManager>,
|
||||
udp_array: RwLock<Option<Arc<UdpSocketArray>>>,
|
||||
try_direct_connect: AtomicBool,
|
||||
punch_predicablely: AtomicBool,
|
||||
punch_randomly: AtomicBool,
|
||||
}
|
||||
|
||||
impl PunchSymToConeHoleClient {
|
||||
pub(crate) fn new(peer_mgr: Arc<PeerManager>) -> 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<Arc<UdpSocketArray>, 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<u16> {
|
||||
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<Controller = BaseController>,
|
||||
>(
|
||||
rpc_stub: S,
|
||||
base_port_for_easy_sym: Option<u16>,
|
||||
my_nat_info: UdpNatType,
|
||||
remote_mapped_addr: crate::proto::common::SocketAddr,
|
||||
public_ips: Vec<Ipv4Addr>,
|
||||
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<Controller = BaseController>,
|
||||
>(
|
||||
rpc_stub: S,
|
||||
remote_mapped_addr: crate::proto::common::SocketAddr,
|
||||
public_ips: Vec<Ipv4Addr>,
|
||||
tid: u32,
|
||||
round: u32,
|
||||
port_index: u32,
|
||||
) -> Option<u32> {
|
||||
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<Controller = BaseController> + std::marker::Send + 'static)> {
|
||||
self.peer_mgr
|
||||
.get_peer_rpc_mgr()
|
||||
.rpc_client()
|
||||
.scoped_client::<UdpHolePunchRpcClientFactory<BaseController>>(
|
||||
self.peer_mgr.my_peer_id(),
|
||||
dst_peer_id,
|
||||
self.peer_mgr.get_global_ctx().get_network_name(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn check_hole_punch_result<T>(
|
||||
udp_array: &Arc<UdpSocketArray>,
|
||||
packet: &[u8],
|
||||
tid: u32,
|
||||
remote_mapped_addr: crate::proto::common::SocketAddr,
|
||||
scoped_punch_task: &ScopedTask<T>,
|
||||
) -> Result<Option<Box<dyn Tunnel>>, anyhow::Error> {
|
||||
// no matter what the result is, we should check if we received any hole punching packet
|
||||
let mut ret_tunnel: Option<Box<dyn Tunnel>> = None;
|
||||
let mut finish_time: Option<Instant> = 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<Option<Box<dyn Tunnel>>, 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::<UdpHolePunchRpcClientFactory<BaseController>>(
|
||||
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<Ipv4Addr> = 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<Option<u32>> =
|
||||
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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<String>,
|
||||
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = t!("core_clap.config_file").to_string()
|
||||
)]
|
||||
config_file: Option<PathBuf>,
|
||||
|
||||
#[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<String>,
|
||||
|
||||
#[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<String>,
|
||||
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = t!("core_clap.external_node").to_string()
|
||||
)]
|
||||
external_node: Option<String>,
|
||||
|
||||
#[arg(
|
||||
short = 'n',
|
||||
long,
|
||||
help = t!("core_clap.proxy_networks").to_string()
|
||||
)]
|
||||
proxy_networks: Vec<String>,
|
||||
|
||||
#[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<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.mapped_listeners").to_string(),
|
||||
num_args = 0..
|
||||
)]
|
||||
mapped_listeners: Vec<String>,
|
||||
|
||||
#[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<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.file_log_level").to_string()
|
||||
)]
|
||||
file_log_level: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.file_log_dir").to_string()
|
||||
)]
|
||||
file_log_dir: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.hostname").to_string()
|
||||
)]
|
||||
hostname: Option<String>,
|
||||
|
||||
#[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<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.default_protocol").to_string()
|
||||
)]
|
||||
default_protocol: Option<String>,
|
||||
|
||||
#[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<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.mtu").to_string()
|
||||
)]
|
||||
mtu: Option<u16>,
|
||||
|
||||
#[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<Ipv4Addr>,
|
||||
|
||||
#[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<Vec<String>>,
|
||||
|
||||
// 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<Vec<String>>,
|
||||
|
||||
#[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<u16>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.ipv6_listener").to_string()
|
||||
)]
|
||||
ipv6_listener: Option<String>,
|
||||
|
||||
#[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<bool>,
|
||||
|
||||
#[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<String>) -> anyhow::Result<Vec<String>> {
|
||||
if no_listener || listeners.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let origin_listners = listeners;
|
||||
let mut listeners: Vec<String> = Vec::new();
|
||||
if origin_listners.len() == 1 {
|
||||
if let Ok(port) = origin_listners[0].parse::<u16>() {
|
||||
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::<url::Url>() {
|
||||
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::<u16>().unwrap()
|
||||
} else {
|
||||
11010 + offset
|
||||
};
|
||||
|
||||
listeners.push(format!("{}://0.0.0.0:{}", proto, port));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(listeners)
|
||||
}
|
||||
|
||||
fn check_tcp_available(port: u16) -> Option<SocketAddr> {
|
||||
let s = format!("0.0.0.0:{}", port).parse::<SocketAddr>().unwrap();
|
||||
TcpSocket::new_v4().unwrap().bind(s).map(|_| s).ok()
|
||||
}
|
||||
|
||||
fn parse_rpc_portal(rpc_portal: String) -> anyhow::Result<SocketAddr> {
|
||||
if let Ok(port) = rpc_portal.parse::<u16>() {
|
||||
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<Self, Self::Error> {
|
||||
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::<PeerConfig>::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::<cidr::Ipv4Cidr>::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::<std::ffi::OsString, _>(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<tokio::sync::Notify>,
|
||||
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<std::ffi::OsString>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
Code is modified from https://github.com/dizda/fast-socks5
|
||||
@@ -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<Socks5Command> {
|
||||
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<AuthenticationMethod> {
|
||||
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<AuthenticationMethod> {
|
||||
// pub fn as_bytes(&self) -> &[u8] {
|
||||
// self.iter().map(|l| l.as_u8()).collect()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//impl From<&[AuthenticationMethod]> for &[u8] {
|
||||
// fn from(_: Vec<AuthenticationMethod>) -> 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<u8>),
|
||||
#[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<T, E = SocksError> = core::result::Result<T, E>;
|
||||
|
||||
/// 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<T: ToTargetAddr>(target_addr: T) -> Result<Vec<u8>> {
|
||||
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))
|
||||
}
|
||||
@@ -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<A: Authentication = DenyAuthentication> {
|
||||
/// 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<Arc<A>>,
|
||||
}
|
||||
|
||||
impl<A: Authentication> Default for Config<A> {
|
||||
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<Self::Item>;
|
||||
}
|
||||
|
||||
/// 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<Self::Item> {
|
||||
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<Self::Item> {
|
||||
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<Self::Item> {
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Authentication> Config<A> {
|
||||
/// 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<T: Authentication + 'static>(self, authentication: T) -> Config<T> {
|
||||
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<Self::S>;
|
||||
}
|
||||
|
||||
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<TcpStream> {
|
||||
tcp_connect_with_timeout(addr, timeout_s).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap TcpStream and contains Socks5 protocol implementation.
|
||||
pub struct Socks5Socket<T: AsyncRead + AsyncWrite + Unpin, A: Authentication, C: AsyncTcpConnector>
|
||||
{
|
||||
inner: T,
|
||||
config: Arc<Config<A>>,
|
||||
auth: AuthenticationMethod,
|
||||
target_addr: Option<TargetAddr>,
|
||||
cmd: Option<Socks5Command>,
|
||||
/// Socket address which will be used in the reply message.
|
||||
reply_ip: Option<IpAddr>,
|
||||
/// If the client has been authenticated, that's where we store his credentials
|
||||
/// to be accessed from the socket
|
||||
credentials: Option<A::Item>,
|
||||
tcp_connector: C,
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin, A: Authentication, C: AsyncTcpConnector>
|
||||
Socks5Socket<T, A, C>
|
||||
{
|
||||
pub fn new(socket: T, config: Arc<Config<A>>, 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<Socks5Socket<T, A, C>> {
|
||||
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<Vec<u8>> {
|
||||
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<u8>) -> Result<u8> {
|
||||
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<A::Item> {
|
||||
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<Socks5Command> {
|
||||
&self.cmd
|
||||
}
|
||||
|
||||
/// Borrow the credentials of the user has authenticated with
|
||||
pub fn get_credentials(&self) -> Option<&<<A as Authentication>::Item as Deref>::Target>
|
||||
where
|
||||
<A as Authentication>::Item: Deref,
|
||||
{
|
||||
self.credentials.as_deref()
|
||||
}
|
||||
|
||||
/// Get the credentials of the user has authenticated with
|
||||
pub fn take_credentials(&mut self) -> Option<A::Item> {
|
||||
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<I, O>(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<T, A: Authentication, S: AsyncTcpConnector> Unpin for Socks5Socket<T, A, S> where
|
||||
T: AsyncRead + AsyncWrite + Unpin
|
||||
{
|
||||
}
|
||||
|
||||
/// Allow us to read directly from the struct
|
||||
impl<T, A: Authentication, S: AsyncTcpConnector> AsyncRead for Socks5Socket<T, A, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_read(context, buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow us to write directly into the struct
|
||||
impl<T, A: Authentication, S: AsyncTcpConnector> AsyncWrite for Socks5Socket<T, A, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.inner).poll_write(context, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_flush(context)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_shutdown(context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate reply code according to the RFC.
|
||||
fn new_reply(error: &ReplyError, sock_addr: SocketAddr) -> Vec<u8> {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod stream;
|
||||
pub mod target_addr;
|
||||
@@ -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<T>(addr: T, request_timeout_s: u64) -> Result<TcpStream>
|
||||
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<T>(addr: T) -> Result<TcpStream>
|
||||
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")] ?
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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<TargetAddr> {
|
||||
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<Vec<u8>> {
|
||||
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<SocketAddr>;
|
||||
|
||||
fn to_socket_addrs(&self) -> io::Result<IntoIter<SocketAddr>> {
|
||||
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<TargetAddr>;
|
||||
}
|
||||
|
||||
impl<'a> ToTargetAddr for (&'a str, u16) {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
// try to parse as an IP first
|
||||
if let Ok(addr) = self.0.parse::<Ipv4Addr>() {
|
||||
return (addr, self.1).to_target_addr();
|
||||
}
|
||||
|
||||
if let Ok(addr) = self.0.parse::<Ipv6Addr>() {
|
||||
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<TargetAddr> {
|
||||
Ok(TargetAddr::Ip(*self))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for SocketAddrV4 {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
SocketAddr::V4(*self).to_target_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for SocketAddrV6 {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
SocketAddr::V6(*self).to_target_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for (Ipv4Addr, u16) {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
SocketAddrV4::new(self.0, self.1).to_target_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for (Ipv6Addr, u16) {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
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<T: AsyncRead + Unpin>(
|
||||
stream: &mut T,
|
||||
atyp: u8,
|
||||
) -> anyhow::Result<TargetAddr> {
|
||||
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)
|
||||
}
|
||||
@@ -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<Self, Error> {
|
||||
Ok(Self {
|
||||
src_peer_id,
|
||||
my_peer_id,
|
||||
src_ip,
|
||||
start_time: std::time::Instant::now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type IcmpNatTable = Arc<dashmap::DashMap<IcmpNatKey, IcmpNatEntry>>;
|
||||
type NewPacketSender = tokio::sync::mpsc::UnboundedSender<IcmpNatKey>;
|
||||
type NewPacketReceiver = tokio::sync::mpsc::UnboundedReceiver<IcmpNatKey>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IcmpProxy {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
|
||||
cidr_set: CidrSet,
|
||||
socket: std::sync::Mutex<Option<socket2::Socket>>,
|
||||
|
||||
nat_table: IcmpNatTable,
|
||||
|
||||
tasks: Mutex<JoinSet<()>>,
|
||||
|
||||
ip_resemmbler: Arc<IpReassembler>,
|
||||
icmp_sender: Arc<std::sync::Mutex<Option<UnboundedSender<ZCPacket>>>>,
|
||||
}
|
||||
|
||||
fn socket_recv(socket: &Socket, buf: &mut [MaybeUninit<u8>]) -> 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<ZCPacket>) {
|
||||
let mut buf = [0u8; 8192];
|
||||
let data: &mut [MaybeUninit<u8>] = 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<ZCPacket> {
|
||||
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<PeerManager>,
|
||||
) -> Result<Arc<Self>, 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<Self>) -> Result<Socket, Error> {
|
||||
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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Vec<u8>>;
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -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<u8>,
|
||||
}
|
||||
|
||||
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<u16>,
|
||||
fragments: Vec<IpFragment>,
|
||||
}
|
||||
|
||||
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<Vec<u8>> {
|
||||
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<IpResemblerKey, IpResemblerValue>,
|
||||
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<Vec<u8>> {
|
||||
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<F>(
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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<KcpEndpoint>,
|
||||
is_src: bool,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl PeerPacketFilter for KcpEndpointFilter {
|
||||
async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option<ZCPacket> {
|
||||
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<PeerManager>,
|
||||
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<KcpEndpoint>,
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NatDstConnector for NatDstKcpConnector {
|
||||
type DstStream = KcpStream;
|
||||
|
||||
async fn connect(&self, src: SocketAddr, nat_dst: SocketAddr) -> Result<Self::DstStream> {
|
||||
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<TcpProxy<NatDstKcpConnector>>);
|
||||
|
||||
pub struct KcpProxySrc {
|
||||
kcp_endpoint: Arc<KcpEndpoint>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
|
||||
tcp_proxy: TcpProxyForKcpSrc,
|
||||
tasks: JoinSet<()>,
|
||||
}
|
||||
|
||||
impl TcpProxyForKcpSrc {
|
||||
async fn check_dst_allow_kcp_input(&self, dst_ip: &Ipv4Addr) -> bool {
|
||||
let peer_map: Arc<crate::peers::peer_map::PeerMap> =
|
||||
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<PeerManager>) -> 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<TcpProxy<NatDstKcpConnector>> {
|
||||
self.tcp_proxy.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KcpProxyDst {
|
||||
kcp_endpoint: Arc<KcpEndpoint>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
proxy_entries: Arc<DashMap<ConnId, TcpProxyEntry>>,
|
||||
tasks: JoinSet<()>,
|
||||
}
|
||||
|
||||
impl KcpProxyDst {
|
||||
pub async fn new(peer_manager: Arc<PeerManager>) -> 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<DashMap<ConnId, TcpProxyEntry>>,
|
||||
) -> 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<DashMap<ConnId, TcpProxyEntry>>);
|
||||
|
||||
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<ListTcpProxyEntryResponse, rpc_types::error::Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<Mutex<Vec<cidr::IpCidr>>>,
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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<std::io::Result<()>> {
|
||||
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<Result<usize, std::io::Error>> {
|
||||
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<Result<(), std::io::Error>> {
|
||||
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<Result<(), std::io::Error>> {
|
||||
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<DashSet<Socks5Entry>>;
|
||||
|
||||
struct Socks5ServerNet {
|
||||
ipv4_addr: cidr::Ipv4Inet,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
|
||||
smoltcp_net: Arc<Net>,
|
||||
forward_tasks: Arc<std::sync::Mutex<JoinSet<()>>>,
|
||||
|
||||
entries: Socks5EntrySet,
|
||||
}
|
||||
|
||||
impl Socks5ServerNet {
|
||||
pub fn new(
|
||||
ipv4_addr: cidr::Ipv4Inet,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
packet_recv: Arc<Mutex<mpsc::Receiver<ZCPacket>>>,
|
||||
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::<AcceptAuthentication>::default();
|
||||
config.set_request_timeout(10);
|
||||
config.set_skip_auth(false);
|
||||
config.set_allow_no_auth(true);
|
||||
|
||||
struct SmolTcpConnector(
|
||||
Arc<Net>,
|
||||
Socks5EntrySet,
|
||||
std::sync::Mutex<Option<Socks5Entry>>,
|
||||
);
|
||||
|
||||
#[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<SocksTcpStream> {
|
||||
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<GlobalCtx>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
|
||||
tasks: Arc<Mutex<JoinSet<()>>>,
|
||||
packet_sender: mpsc::Sender<ZCPacket>,
|
||||
packet_recv: Arc<Mutex<mpsc::Receiver<ZCPacket>>>,
|
||||
|
||||
net: Arc<Mutex<Option<Socks5ServerNet>>>,
|
||||
entries: Socks5EntrySet,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl PeerPacketFilter for Socks5Server {
|
||||
async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option<ZCPacket> {
|
||||
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<GlobalCtx>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
) -> Arc<Self> {
|
||||
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<Self>) {
|
||||
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<Self>) -> 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::<SocketAddr>().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(())
|
||||
}
|
||||
}
|
||||
@@ -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<Self::DstStream>;
|
||||
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<Self::DstStream> {
|
||||
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<chrono::Local>,
|
||||
tasks: Mutex<JoinSet<()>>,
|
||||
state: AtomicCell<NatDstEntryState>,
|
||||
}
|
||||
|
||||
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<D: AsyncRead + AsyncWrite + Unpin>(
|
||||
&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<Result<(tokio_smoltcp::TcpStream, SocketAddr)>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "smoltcp")]
|
||||
impl SmolTcpListener {
|
||||
pub async fn new(net: Arc<Mutex<Option<Net>>>, 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<NatDstEntry>;
|
||||
|
||||
type SynSockMap = Arc<DashMap<SocketAddr, ArcNatDstEntry>>;
|
||||
type ConnSockMap = Arc<DashMap<uuid::Uuid, ArcNatDstEntry>>;
|
||||
// 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<DashMap<SocketAddr, ArcNatDstEntry>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TcpProxy<C: NatDstConnector> {
|
||||
global_ctx: Arc<GlobalCtx>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
local_port: AtomicU16,
|
||||
|
||||
tasks: Arc<std::sync::Mutex<JoinSet<()>>>,
|
||||
|
||||
syn_map: SynSockMap,
|
||||
conn_map: ConnSockMap,
|
||||
addr_conn_map: AddrConnSockMap,
|
||||
|
||||
cidr_set: CidrSet,
|
||||
|
||||
smoltcp_stack_sender: Option<mpsc::Sender<ZCPacket>>,
|
||||
smoltcp_stack_receiver: Arc<Mutex<Option<mpsc::Receiver<ZCPacket>>>>,
|
||||
#[cfg(feature = "smoltcp")]
|
||||
smoltcp_net: Arc<Mutex<Option<Net>>>,
|
||||
enable_smoltcp: Arc<AtomicBool>,
|
||||
|
||||
connector: C,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<C: NatDstConnector> PeerPacketFilter for TcpProxy<C> {
|
||||
async fn try_process_packet_from_peer(&self, mut packet: ZCPacket) -> Option<ZCPacket> {
|
||||
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<C: NatDstConnector> NicPacketFilter for TcpProxy<C> {
|
||||
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<C: NatDstConnector> TcpProxy<C> {
|
||||
pub fn new(peer_manager: Arc<PeerManager>, connector: C) -> Arc<Self> {
|
||||
let (smoltcp_stack_sender, smoltcp_stack_receiver) = mpsc::channel::<ZCPacket>(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<Self>, 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<ProxyTcpListener> {
|
||||
#[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<Ipv4Addr> {
|
||||
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<PeerManager> {
|
||||
&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<TcpProxyEntry> {
|
||||
let mut entries: Vec<TcpProxyEntry> = 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<C: NatDstConnector> {
|
||||
tcp_proxy: Weak<TcpProxy<C>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<C: NatDstConnector> TcpProxyRpc for TcpProxyRpcService<C> {
|
||||
type Controller = BaseController;
|
||||
async fn list_tcp_proxy_entry(
|
||||
&self,
|
||||
_: BaseController,
|
||||
_request: ListTcpProxyEntryRequest, // Accept request of type HelloRequest
|
||||
) -> std::result::Result<ListTcpProxyEntryResponse, rpc_types::error::Error> {
|
||||
let mut reply = ListTcpProxyEntryResponse::default();
|
||||
if let Some(tcp_proxy) = self.tcp_proxy.upgrade() {
|
||||
reply.entries = tcp_proxy.list_proxy_entries();
|
||||
}
|
||||
Ok(reply)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: NatDstConnector> TcpProxyRpcService<C> {
|
||||
pub fn new(tcp_proxy: Arc<TcpProxy<C>>) -> Self {
|
||||
Self {
|
||||
tcp_proxy: Arc::downgrade(&tcp_proxy),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<io::Result<Vec<u8>>>,
|
||||
send: PollSender<Vec<u8>>,
|
||||
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<io::Result<Vec<u8>>>, Receiver<Vec<u8>>) {
|
||||
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<Vec<u8>>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
self.recv.poll_recv(cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn map_err(e: PollSendError<Vec<u8>>) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::Other, e)
|
||||
}
|
||||
|
||||
impl Sink<Vec<u8>> for ChannelDevice {
|
||||
type Error = io::Error;
|
||||
|
||||
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.send.poll_reserve(cx).map_err(map_err)
|
||||
}
|
||||
|
||||
fn start_send(mut self: Pin<&mut Self>, item: Vec<u8>) -> Result<(), Self::Error> {
|
||||
self.send.send_item(item).map_err(map_err)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.send.poll_reserve(cx).map_err(map_err)
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncDevice for ChannelDevice {
|
||||
fn capabilities(&self) -> &DeviceCapabilities {
|
||||
&self.caps
|
||||
}
|
||||
}
|
||||
@@ -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<u8>;
|
||||
|
||||
/// A device that send and receive packets asynchronously.
|
||||
pub trait AsyncDevice:
|
||||
Stream<Item = io::Result<Packet>> + Sink<Packet, Error = io::Error> + Send + Unpin
|
||||
{
|
||||
/// Returns the device capabilities.
|
||||
fn capabilities(&self) -> &DeviceCapabilities;
|
||||
}
|
||||
|
||||
impl<T> AsyncDevice for Box<T>
|
||||
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<Packet>,
|
||||
send_queue: VecDeque<Packet>,
|
||||
}
|
||||
|
||||
/// RxToken for `BufferDevice`.
|
||||
pub struct BufferRxToken(Packet);
|
||||
|
||||
impl RxToken for BufferRxToken {
|
||||
fn consume<R, F>(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<R, F>(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<Self::TxToken<'_>> {
|
||||
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<Packet> {
|
||||
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<Item = Packet>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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<IpAddress>,
|
||||
pub buffer_size: BufferSize,
|
||||
}
|
||||
|
||||
impl NetConfig {
|
||||
pub fn new(interface_config: Config, ip_addr: IpCidr, gateway: Vec<IpAddress>) -> 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<Reactor>,
|
||||
ip_addr: IpCidr,
|
||||
from_port: AtomicU16,
|
||||
stopper: Arc<Notify>,
|
||||
}
|
||||
|
||||
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<D: device::AsyncDevice + 'static>(device: D, config: NetConfig) -> Net {
|
||||
let (net, fut) = Self::new2(device, config);
|
||||
tokio::spawn(fut);
|
||||
net
|
||||
}
|
||||
|
||||
fn new2<D: device::AsyncDevice + 'static>(
|
||||
device: D,
|
||||
config: NetConfig,
|
||||
) -> (Net, impl Future<Output = io::Result<()>> + 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<TcpListener> {
|
||||
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> {
|
||||
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<F: FnOnce(&Routes)>(&self, f: F) {
|
||||
let iface = self.reactor.iface().clone();
|
||||
let iface = iface.lock();
|
||||
let routes = iface.routes();
|
||||
f(routes)
|
||||
}
|
||||
|
||||
pub fn routes_mut<F: FnOnce(&mut Routes)>(&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()
|
||||
}
|
||||
}
|
||||
@@ -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<Mutex<Interface>>;
|
||||
const MAX_BURST_SIZE: usize = 100;
|
||||
|
||||
pub(crate) struct Reactor {
|
||||
notify: Arc<Notify>,
|
||||
iface: BufferInterface,
|
||||
socket_allocator: SocketAlloctor,
|
||||
}
|
||||
|
||||
async fn receive(
|
||||
async_iface: &mut impl super::device::AsyncDevice,
|
||||
recv_buf: &mut VecDeque<Packet>,
|
||||
) -> 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<Notify>,
|
||||
stopper: Arc<Notify>,
|
||||
) -> 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<Notify>,
|
||||
) -> (Self, impl Future<Output = io::Result<()>> + 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<T: AnySocket<'static>>(
|
||||
&self,
|
||||
handle: SocketHandle,
|
||||
) -> MappedMutexGuard<'_, T> {
|
||||
MutexGuard::map(
|
||||
self.socket_allocator.sockets().lock(),
|
||||
|sockets: &mut smoltcp::iface::SocketSet<'_>| sockets.get_mut::<T>(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)]
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Reactor>,
|
||||
local_addr: SocketAddr,
|
||||
}
|
||||
|
||||
fn map_err<E: std::error::Error>(e: E) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::Other, e.to_string())
|
||||
}
|
||||
|
||||
impl TcpListener {
|
||||
pub(super) async fn new(
|
||||
reactor: Arc<Reactor>,
|
||||
local_endpoint: IpEndpoint,
|
||||
) -> io::Result<TcpListener> {
|
||||
let handle = reactor.socket_allocator().new_tcp_socket();
|
||||
{
|
||||
let mut socket = reactor.get_socket::<tcp::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<io::Result<(TcpStream, SocketAddr)>> {
|
||||
let mut socket = self.reactor.get_socket::<tcp::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<SocketAddr> {
|
||||
Ok(self.local_addr)
|
||||
}
|
||||
|
||||
pub fn relisten(&mut self) {
|
||||
let mut socket = self.reactor.get_socket::<tcp::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::<tcp::Socket>(*self.handle);
|
||||
socket.is_listening()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Incoming(TcpListener);
|
||||
|
||||
impl Stream for Incoming {
|
||||
type Item = io::Result<TcpStream>;
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
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<Reactor>,
|
||||
local_addr: SocketAddr,
|
||||
peer_addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl TcpStream {
|
||||
pub(super) async fn connect(
|
||||
reactor: Arc<Reactor>,
|
||||
local_endpoint: IpEndpoint,
|
||||
remote_endpoint: IpEndpoint,
|
||||
) -> io::Result<TcpStream> {
|
||||
let handle = reactor.socket_allocator().new_tcp_socket();
|
||||
|
||||
reactor
|
||||
.get_socket::<tcp::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::<tcp::Socket>(*new_handle);
|
||||
new_socket.listen(listener.local_addr).map_err(map_err)?;
|
||||
}
|
||||
let (peer_addr, local_addr) = {
|
||||
let socket = reactor.get_socket::<tcp::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<SocketAddr> {
|
||||
Ok(self.local_addr)
|
||||
}
|
||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok(self.peer_addr)
|
||||
}
|
||||
pub fn poll_connected(&self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
let mut socket = self.reactor.get_socket::<tcp::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<io::Result<()>> {
|
||||
let mut socket = self.reactor.get_socket::<tcp::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<Result<usize, io::Error>> {
|
||||
let mut socket = self.reactor.get_socket::<tcp::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<Result<(), io::Error>> {
|
||||
let mut socket = self.reactor.get_socket::<tcp::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<Result<(), io::Error>> {
|
||||
let mut socket = self.reactor.get_socket::<tcp::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
|
||||
}
|
||||
}
|
||||
@@ -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<Mutex<SocketSet<'static>>>;
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
@@ -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<Option<JoinHandle<()>>>,
|
||||
stopped: AtomicBool,
|
||||
start_time: std::time::Instant,
|
||||
last_active_time: AtomicCell<std::time::Instant>,
|
||||
}
|
||||
|
||||
impl UdpNatEntry {
|
||||
#[tracing::instrument(err(level = Level::WARN))]
|
||||
fn new(src_peer_id: PeerId, my_peer_id: PeerId, src_socket: SocketAddr) -> Result<Self, Error> {
|
||||
// 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<Self>,
|
||||
packet_sender: &mut Sender<ZCPacket>,
|
||||
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<Self>,
|
||||
mut packet_sender: Sender<ZCPacket>,
|
||||
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<PeerManager>,
|
||||
|
||||
cidr_set: CidrSet,
|
||||
|
||||
nat_table: Arc<DashMap<UdpNatKey, Arc<UdpNatEntry>>>,
|
||||
|
||||
sender: Sender<ZCPacket>,
|
||||
receiver: Mutex<Option<Receiver<ZCPacket>>>,
|
||||
|
||||
tasks: Mutex<JoinSet<()>>,
|
||||
|
||||
ip_resemmbler: Arc<IpReassembler>,
|
||||
}
|
||||
|
||||
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<Vec<u8>>;
|
||||
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::<Error>(|| {
|
||||
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<ZCPacket> {
|
||||
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<PeerManager>,
|
||||
) -> Result<Arc<Self>, 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<Self>) -> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<TcpProxy<NatDstTcpConnector>>,
|
||||
icmp_proxy: Arc<IcmpProxy>,
|
||||
udp_proxy: Arc<UdpProxy>,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
started: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl IpProxy {
|
||||
fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc<PeerManager>) -> Result<Self, Error> {
|
||||
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<PeerManager>,
|
||||
_peer_packet_receiver: Arc<Mutex<PacketRecvChanReceiver>>,
|
||||
) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub async fn run(&mut self, _ipv4_addr: Ipv4Addr) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
type ArcNicCtx = Arc<Mutex<Option<Box<dyn Any + 'static + Send>>>>;
|
||||
|
||||
pub struct Instance {
|
||||
inst_name: String,
|
||||
|
||||
id: uuid::Uuid,
|
||||
|
||||
nic_ctx: ArcNicCtx,
|
||||
|
||||
peer_packet_receiver: Arc<Mutex<PacketRecvChanReceiver>>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
listener_manager: Arc<Mutex<ListenerManager<PeerManager>>>,
|
||||
conn_manager: Arc<ManualConnectorManager>,
|
||||
direct_conn_manager: Arc<DirectConnectorManager>,
|
||||
udp_hole_puncher: Arc<Mutex<UdpHolePunchConnector>>,
|
||||
|
||||
ip_proxy: Option<IpProxy>,
|
||||
|
||||
kcp_proxy_src: Option<KcpProxySrc>,
|
||||
kcp_proxy_dst: Option<KcpProxyDst>,
|
||||
|
||||
peer_center: Arc<PeerCenterInstance>,
|
||||
|
||||
vpn_portal: Arc<Mutex<Box<dyn VpnPortal>>>,
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
socks5_server: Arc<Socks5Server>,
|
||||
|
||||
rpc_server: Option<StandAloneServer<TcpTunnelListener>>,
|
||||
|
||||
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<ManualConnectorManager> {
|
||||
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<Mutex<PacketRecvChanReceiver>>,
|
||||
) {
|
||||
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<Ipv4Inet> = 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<PeerManager> {
|
||||
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<Controller = BaseController> + Clone {
|
||||
#[derive(Clone)]
|
||||
struct VpnPortalRpcService {
|
||||
peer_mgr: Weak<PeerManager>,
|
||||
vpn_portal: Weak<Mutex<Box<dyn VpnPortal>>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl VpnPortalRpc for VpnPortalRpcService {
|
||||
type Controller = BaseController;
|
||||
|
||||
async fn get_vpn_portal_info(
|
||||
&self,
|
||||
_: BaseController,
|
||||
_request: GetVpnPortalInfoRequest,
|
||||
) -> Result<GetVpnPortalInfoResponse, rpc_types::error::Error> {
|
||||
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<Mutex<Box<dyn VpnPortal>>> {
|
||||
self.vpn_portal.clone()
|
||||
}
|
||||
|
||||
pub fn get_nic_ctx(&self) -> ArcNicCtx {
|
||||
self.nic_ctx.clone()
|
||||
}
|
||||
|
||||
pub fn get_peer_packet_receiver(&self) -> Arc<Mutex<PacketRecvChanReceiver>> {
|
||||
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<PeerManager>,
|
||||
peer_packet_receiver: Arc<Mutex<PacketRecvChanReceiver>>,
|
||||
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(())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user