diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..3aa0aa54 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,170 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +tab_width = 4 +indent_size = 4 +end_of_line = crlf +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.cs] +dotnet_hide_advanced_members = true +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_property_generation_behavior = prefer_throwing_properties +dotnet_search_reference_assemblies = true +dotnet_separate_import_directive_groups = false:warning +dotnet_sort_system_directives_first = true:warning +file_header_template = unset + +dotnet_style_qualification_for_event = false:warning +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_operators = always_for_clarity:warning +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning +dotnet_style_require_accessibility_modifiers = always:warning +dotnet_prefer_system_hash_code = true:warning +dotnet_style_coalesce_expression = true:warning +dotnet_style_collection_initializer = false:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_namespace_match_folder = true:warning +dotnet_style_null_propagation = true:warning +dotnet_style_object_initializer = true:warning +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_collection_expression = false:warning +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = false:warning +dotnet_style_prefer_conditional_expression_over_return = false:warning +dotnet_style_prefer_foreach_explicit_cast_in_source = always:warning +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_inferred_tuple_names = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning +dotnet_style_prefer_simplified_boolean_expressions = true:warning +dotnet_style_prefer_simplified_interpolation = true:warning +dotnet_style_readonly_field = true:warning +dotnet_code_quality_unused_parameters = all:warning +dotnet_remove_unnecessary_suppression_exclusions = none +dotnet_style_allow_multiple_blank_lines_experimental = false:warning +dotnet_style_allow_statement_immediately_after_block_experimental = true:warning + +csharp_style_var_elsewhere = true:warning +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_style_expression_bodied_accessors = when_on_single_line:warning +csharp_style_expression_bodied_constructors = false:warning +csharp_style_expression_bodied_indexers = when_on_single_line:warning +csharp_style_expression_bodied_lambdas = when_on_single_line:warning +csharp_style_expression_bodied_local_functions = false:warning +csharp_style_expression_bodied_methods = false:warning +csharp_style_expression_bodied_operators = false:warning +csharp_style_expression_bodied_properties = when_on_single_line:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_prefer_extended_property_pattern = true:warning +csharp_style_prefer_not_pattern = true:warning +csharp_style_prefer_pattern_matching = true:warning +csharp_style_prefer_switch_expression = false:warning +csharp_style_conditional_delegate_call = true:warning +csharp_prefer_static_anonymous_function = true:warning +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,internal,private,protected,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:warning +csharp_style_prefer_readonly_struct = true:warning +csharp_style_prefer_readonly_struct_member = true:warning +csharp_prefer_braces = true:warning +csharp_prefer_simple_using_statement = true:warning +csharp_prefer_system_threading_lock = true:warning +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_prefer_method_group_conversion = true:warning +csharp_style_prefer_primary_constructors = true:warning +csharp_style_prefer_top_level_statements = false:warning +csharp_prefer_simple_default_expression = true:warning +csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_implicit_object_creation_when_type_is_apparent = true:warning +csharp_style_inlined_variable_declaration = true:warning +csharp_style_prefer_index_operator = false:warning +csharp_style_prefer_local_over_anonymous_function = true:warning +csharp_style_prefer_null_check_over_type_check = true:warning +csharp_style_prefer_range_operator = false:warning +csharp_style_prefer_tuple_swap = true:warning +csharp_style_prefer_utf8_string_literals = true:warning +csharp_style_throw_expression = true:warning +csharp_style_unused_value_assignment_preference = discard_variable:warning +csharp_style_unused_value_expression_statement_preference = discard_variable:warning +csharp_using_directive_placement = outside_namespace:warning +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:warning +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:warning +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false:warning +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:warning +csharp_style_allow_embedded_statements_on_same_line_experimental = false:warning + +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = no_change +csharp_indent_switch_labels = true +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false:warning +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false + +dotnet_naming_rule.interface_should_be_pascal.severity = warning +dotnet_naming_rule.interface_should_be_pascal.symbols = interface +dotnet_naming_rule.interface_should_be_pascal.style = pascal +dotnet_naming_rule.types_should_be_pascal.severity = warning +dotnet_naming_rule.types_should_be_pascal.symbols = types +dotnet_naming_rule.types_should_be_pascal.style = pascal +dotnet_naming_rule.non_field_members_should_be_pascal.severity = warning +dotnet_naming_rule.non_field_members_should_be_pascal.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal.style = pascal +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = * +dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = * +dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = * +dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_style.pascal.required_prefix = +dotnet_naming_style.pascal.required_suffix = +dotnet_naming_style.pascal.word_separator = +dotnet_naming_style.pascal.capitalization = pascal_case \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/01_bug_report.yml new file mode 100644 index 00000000..e96682bd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_bug_report.yml @@ -0,0 +1,65 @@ +name: Bug 报告 +description: 在提出问题前请先自行排除服务器端问题和升级到最新客户端,同时也请通过搜索确认是否有人提出过相同问题。 +title: "[Bug]: " +labels: ["bug"] +body: + - type: input + id: "os-version" + attributes: + label: "操作系统和版本" + description: "操作系统和版本" + validations: + required: true + - type: input + id: "expectation" + attributes: + label: "预期情况" + description: "描述你认为应该发生什么" + validations: + required: true + - type: textarea + id: "describe-the-bug" + attributes: + label: "实际情况" + description: "描述实际发生了什么" + validations: + required: true + - type: textarea + id: "reproduction-method" + attributes: + label: "复现方法" + description: "在BUG出现前执行了哪些操作" + placeholder: 标序号 + validations: + required: true + - type: textarea + id: "log" + attributes: + label: "日志信息" + description: "位置在软件当前目录下的guiLogs" + placeholder: 在日志开始和结束位置粘贴冒号后的内容:``` + validations: + required: true + - type: textarea + id: "more" + attributes: + label: "额外信息" + description: "可选" + validations: + required: false + - type: checkboxes + id: "latest-version" + attributes: + label: "我确认已更新至最新版本" + description: "否则请更新后尝试" + options: + - label: 是 + required: true + - type: checkboxes + id: "issues" + attributes: + label: "我确认已查询历史issues" + description: "否则请查询后提出" + options: + - label: 是 + required: true diff --git a/.github/ISSUE_TEMPLATE/02_feature_request.yml b/.github/ISSUE_TEMPLATE/02_feature_request.yml new file mode 100644 index 00000000..4e79294c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_feature_request.yml @@ -0,0 +1,34 @@ +name: Feature 请求 +description: "为这个项目提出一个建议" +title: "[Feature request]: " +labels: ['enhancement'] +body: +- type: input + id: problem + attributes: + label: 相关问题 + description: "清楚而简洁地描述问题是什么。" + placeholder: "当我想要……时,软件不能……" + validations: + required: true +- type: input + id: way-to-solve + attributes: + label: 描述你希望的解决方案 + description: "你希望发生什么" + validations: + required: true +- type: input + id: instead + attributes: + label: 描述你所考虑的替代方案 + validations: + required: false +- type: checkboxes + id: "issues" + attributes: + label: "我确认已查询历史issues" + description: "否则请查询后提出" + options: + - label: 是 + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..00fd44bd --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index 79efbbc9..00000000 --- a/.github/issue_template.md +++ /dev/null @@ -1,25 +0,0 @@ -在提出问题前请先自行排除服务器端问题和升级到最新客户端,同时也请通过搜索确认是否有人提出过相同问题。 - -### 预期行为 -描述你认为应该发生什么 - -### 实际行为 -描述实际发生了什么 - -### 复现方法 -1. -2. -3. - -### 日志信息,位置在当前目录下的guiLogs -
- -``` -在这里粘贴日志 -``` -
- -### 环境信息(客户端请升级至最新正式版) - -### 额外信息(可选) - diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml new file mode 100644 index 00000000..03b7d834 --- /dev/null +++ b/.github/workflows/build-all.yml @@ -0,0 +1,69 @@ +name: release all platforms + +on: + workflow_dispatch: + inputs: + release_tag: + required: false + type: string + +jobs: + update: + runs-on: ubuntu-latest + steps: + + - name: Trigger build windows + if: github.event.inputs.release_tag != '' + run: | + curl -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + https://api.github.com/repos/${{ github.repository }}/actions/workflows/build-windows.yml/dispatches \ + -d "{ + \"ref\": \"master\", + \"inputs\": { + \"release_tag\": \"${{ github.event.inputs.release_tag }}\" + } + }" + + - name: Trigger build linux + if: github.event.inputs.release_tag != '' + run: | + curl -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + https://api.github.com/repos/${{ github.repository }}/actions/workflows/build-linux.yml/dispatches \ + -d "{ + \"ref\": \"master\", + \"inputs\": { + \"release_tag\": \"${{ github.event.inputs.release_tag }}\" + } + }" + + - name: Trigger build osx + if: github.event.inputs.release_tag != '' + run: | + curl -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + https://api.github.com/repos/${{ github.repository }}/actions/workflows/build-osx.yml/dispatches \ + -d "{ + \"ref\": \"master\", + \"inputs\": { + \"release_tag\": \"${{ github.event.inputs.release_tag }}\" + } + }" + + - name: Trigger build windows desktop + if: github.event.inputs.release_tag != '' + run: | + curl -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + https://api.github.com/repos/${{ github.repository }}/actions/workflows/build-windows-desktop.yml/dispatches \ + -d "{ + \"ref\": \"master\", + \"inputs\": { + \"release_tag\": \"${{ github.event.inputs.release_tag }}\" + } + }" \ No newline at end of file diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 00000000..40e9c953 --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,135 @@ +name: release Linux + +on: + workflow_dispatch: + inputs: + release_tag: + required: false + type: string + push: + branches: + - master + +env: + OutputArch: "linux-64" + OutputArchArm: "linux-arm64" + OutputPath64: "${{ github.workspace }}/v2rayN/Release/linux-64" + OutputPathArm64: "${{ github.workspace }}/v2rayN/Release/linux-arm64" + +jobs: + build: + strategy: + matrix: + configuration: [Release] + + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v5.0.0 + with: + submodules: 'recursive' + fetch-depth: '0' + + - name: Setup + uses: actions/setup-dotnet@v5.0.0 + with: + dotnet-version: '8.0.x' + + - name: Build + run: | + cd v2rayN + dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r linux-x64 --self-contained=true -o $OutputPath64 + dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r linux-arm64 --self-contained=true -o $OutputPathArm64 + dotnet publish ./AmazTool/AmazTool.csproj -c Release -r linux-x64 --self-contained=true -p:PublishTrimmed=true -o $OutputPath64 + dotnet publish ./AmazTool/AmazTool.csproj -c Release -r linux-arm64 --self-contained=true -p:PublishTrimmed=true -o $OutputPathArm64 + + - name: Upload build artifacts + uses: actions/upload-artifact@v4.6.2 + with: + name: v2rayN-linux + path: | + ${{ github.workspace }}/v2rayN/Release/linux* + + # release debian package + - name: Package debian + if: github.event.inputs.release_tag != '' + run: | + chmod 755 package-debian.sh + ./package-debian.sh $OutputArch $OutputPath64 ${{ github.event.inputs.release_tag }} + ./package-debian.sh $OutputArchArm $OutputPathArm64 ${{ github.event.inputs.release_tag }} + + - name: Upload deb to release + uses: svenstaro/upload-release-action@v2 + if: github.event.inputs.release_tag != '' + with: + file: ${{ github.workspace }}/v2rayN*.deb + tag: ${{ github.event.inputs.release_tag }} + file_glob: true + prerelease: true + + - name: Package AppImage + if: github.event.inputs.release_tag != '' + run: | + chmod a+x package-appimage.sh + ./package-appimage.sh + + - name: Upload AppImage to release + uses: svenstaro/upload-release-action@v2 + if: github.event.inputs.release_tag != '' + with: + file: ${{ github.workspace }}/v2rayN*.AppImage + tag: ${{ github.event.inputs.release_tag }} + file_glob: true + prerelease: true + + # release zip archive + - name: Package release zip archive + if: github.event.inputs.release_tag != '' + run: | + chmod 755 package-release-zip.sh + ./package-release-zip.sh $OutputArch $OutputPath64 + ./package-release-zip.sh $OutputArchArm $OutputPathArm64 + + - name: Upload zip archive to release + uses: svenstaro/upload-release-action@v2 + if: github.event.inputs.release_tag != '' + with: + file: ${{ github.workspace }}/v2rayN*.zip + tag: ${{ github.event.inputs.release_tag }} + file_glob: true + prerelease: true + + # release RHEL package + - name: Package RPM (RHEL-family) + if: github.event.inputs.release_tag != '' + run: | + chmod 755 package-rhel.sh + # Build for both x86_64 and aarch64 in one go (explicit version passed; no --buildfrom) + ./package-rhel.sh "${{ github.event.inputs.release_tag }}" --arch all + + - name: Collect RPMs into workspace + if: github.event.inputs.release_tag != '' + run: | + mkdir -p "${{ github.workspace }}/dist/rpm" + rsync -av "$HOME/rpmbuild/RPMS/" "${{ github.workspace }}/dist/rpm/" + # Rename to requested filenames + find "${{ github.workspace }}/dist/rpm" -name "v2rayN-*-1.x86_64.rpm" -exec mv {} "${{ github.workspace }}/dist/rpm/v2rayN-linux-rhel-x64.rpm" \; || true + find "${{ github.workspace }}/dist/rpm" -name "v2rayN-*-1.aarch64.rpm" -exec mv {} "${{ github.workspace }}/dist/rpm/v2rayN-linux-rhel-arm64.rpm" \; || true + + - name: Upload RPM artifacts + if: github.event.inputs.release_tag != '' + uses: actions/upload-artifact@v4.6.2 + with: + name: v2rayN-rpm + path: | + ${{ github.workspace }}/dist/rpm/**/*.rpm + + - name: Upload RPMs to release + uses: svenstaro/upload-release-action@v2 + if: github.event.inputs.release_tag != '' + with: + file: ${{ github.workspace }}/dist/rpm/**/*.rpm + tag: ${{ github.event.inputs.release_tag }} + file_glob: true + prerelease: true diff --git a/.github/workflows/build-osx.yml b/.github/workflows/build-osx.yml new file mode 100644 index 00000000..97c002c7 --- /dev/null +++ b/.github/workflows/build-osx.yml @@ -0,0 +1,87 @@ +name: release macOS + +on: + workflow_dispatch: + inputs: + release_tag: + required: false + type: string + push: + branches: + - master + +env: + OutputArch: "macos-64" + OutputArchArm: "macos-arm64" + OutputPath64: "${{ github.workspace }}/v2rayN/Release/macos-64" + OutputPathArm64: "${{ github.workspace }}/v2rayN/Release/macos-arm64" + +jobs: + build: + strategy: + matrix: + configuration: [Release] + + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v5.0.0 + with: + submodules: 'recursive' + fetch-depth: '0' + + - name: Setup + uses: actions/setup-dotnet@v5.0.0 + with: + dotnet-version: '8.0.x' + + - name: Build + run: | + cd v2rayN + dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r osx-x64 --self-contained=true -o $OutputPath64 + dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r osx-arm64 --self-contained=true -o $OutputPathArm64 + dotnet publish ./AmazTool/AmazTool.csproj -c Release -r osx-x64 --self-contained=true -p:PublishTrimmed=true -o $OutputPath64 + dotnet publish ./AmazTool/AmazTool.csproj -c Release -r osx-arm64 --self-contained=true -p:PublishTrimmed=true -o $OutputPathArm64 + + - name: Upload build artifacts + uses: actions/upload-artifact@v4.6.2 + with: + name: v2rayN-macos + path: | + ${{ github.workspace }}/v2rayN/Release/macos* + + # release osx package + - name: Package osx + if: github.event.inputs.release_tag != '' + run: | + brew install create-dmg + chmod 755 package-osx.sh + ./package-osx.sh $OutputArch $OutputPath64 ${{ github.event.inputs.release_tag }} + ./package-osx.sh $OutputArchArm $OutputPathArm64 ${{ github.event.inputs.release_tag }} + + - name: Upload dmg to release + uses: svenstaro/upload-release-action@v2 + if: github.event.inputs.release_tag != '' + with: + file: ${{ github.workspace }}/v2rayN*.dmg + tag: ${{ github.event.inputs.release_tag }} + file_glob: true + prerelease: true + + # release zip archive + - name: Package release zip archive + if: github.event.inputs.release_tag != '' + run: | + chmod 755 package-release-zip.sh + ./package-release-zip.sh $OutputArch $OutputPath64 + ./package-release-zip.sh $OutputArchArm $OutputPathArm64 + + - name: Upload zip archive to release + uses: svenstaro/upload-release-action@v2 + if: github.event.inputs.release_tag != '' + with: + file: ${{ github.workspace }}/v2rayN*.zip + tag: ${{ github.event.inputs.release_tag }} + file_glob: true + prerelease: true \ No newline at end of file diff --git a/.github/workflows/build-windows-desktop.yml b/.github/workflows/build-windows-desktop.yml new file mode 100644 index 00000000..3b28599d --- /dev/null +++ b/.github/workflows/build-windows-desktop.yml @@ -0,0 +1,71 @@ +name: release Windows desktop (Avalonia UI) + +on: + workflow_dispatch: + inputs: + release_tag: + required: false + type: string + push: + branches: + - master + +env: + OutputArch: "windows-64" + OutputArchArm: "windows-arm64" + OutputPath64: "${{ github.workspace }}/v2rayN/Release/windows-64" + OutputPathArm64: "${{ github.workspace }}/v2rayN/Release/windows-arm64" + +jobs: + build: + strategy: + matrix: + configuration: [Release] + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5.0.0 + with: + submodules: 'recursive' + fetch-depth: '0' + + - name: Setup + uses: actions/setup-dotnet@v5.0.0 + with: + dotnet-version: '8.0.x' + + - name: Build + run: | + cd v2rayN + dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r win-x64 --self-contained=true -p:EnableWindowsTargeting=true -o $OutputPath64 + dotnet publish ./v2rayN.Desktop/v2rayN.Desktop.csproj -c Release -r win-arm64 --self-contained=true -p:EnableWindowsTargeting=true -o $OutputPathArm64 + dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-x64 --self-contained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPath64 + dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 --self-contained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64 + + - name: Upload build artifacts + uses: actions/upload-artifact@v4.6.2 + with: + name: v2rayN-windows-desktop + path: | + ${{ github.workspace }}/v2rayN/Release/windows* + + # release zip archive + - name: Package release zip archive + if: github.event.inputs.release_tag != '' + run: | + chmod 755 package-release-zip.sh + ./package-release-zip.sh $OutputArch $OutputPath64 + mv "v2rayN-${OutputArch}.zip" "v2rayN-${OutputArch}-desktop.zip" + ./package-release-zip.sh $OutputArchArm $OutputPathArm64 + mv "v2rayN-${OutputArchArm}.zip" "v2rayN-${OutputArchArm}-desktop.zip" + + - name: Upload zip archive to release + uses: svenstaro/upload-release-action@v2 + if: github.event.inputs.release_tag != '' + with: + file: ${{ github.workspace }}/v2rayN*.zip + tag: ${{ github.event.inputs.release_tag }} + file_glob: true + prerelease: true diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml new file mode 100644 index 00000000..fea3aa70 --- /dev/null +++ b/.github/workflows/build-windows.yml @@ -0,0 +1,71 @@ +name: release Windows + +on: + workflow_dispatch: + inputs: + release_tag: + required: false + type: string + push: + branches: + - master + +env: + OutputArch: "windows-64" + OutputArchArm: "windows-arm64" + OutputPath64: "${{ github.workspace }}/v2rayN/Release/windows-64" + OutputPathArm64: "${{ github.workspace }}/v2rayN/Release/windows-arm64" + OutputPath64Sc: "${{ github.workspace }}/v2rayN/Release/windows-64-SelfContained" + +jobs: + build: + strategy: + matrix: + configuration: [Release] + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5.0.0 + + - name: Setup + uses: actions/setup-dotnet@v5.0.0 + with: + dotnet-version: '8.0.x' + + - name: Build + run: | + cd v2rayN + dotnet publish ./v2rayN/v2rayN.csproj -c Release -r win-x64 --self-contained=false -p:EnableWindowsTargeting=true -o $OutputPath64 + dotnet publish ./v2rayN/v2rayN.csproj -c Release -r win-arm64 --self-contained=false -p:EnableWindowsTargeting=true -o $OutputPathArm64 + dotnet publish ./v2rayN/v2rayN.csproj -c Release -r win-x64 --self-contained=true -p:EnableWindowsTargeting=true -o $OutputPath64Sc + dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-x64 --self-contained=false -p:EnableWindowsTargeting=true -o $OutputPath64 + dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 --self-contained=false -p:EnableWindowsTargeting=true -o $OutputPathArm64 + dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-x64 --self-contained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPath64Sc + + + - name: Upload build artifacts + uses: actions/upload-artifact@v4.6.2 + with: + name: v2rayN-windows + path: | + ${{ github.workspace }}/v2rayN/Release/windows* + + # release zip archive + - name: Package release zip archive + if: github.event.inputs.release_tag != '' + run: | + chmod 755 package-release-zip.sh + ./package-release-zip.sh $OutputArch $OutputPath64 + ./package-release-zip.sh $OutputArchArm $OutputPathArm64 + ./package-release-zip.sh "windows-64-SelfContained" $OutputPath64Sc + + - name: Upload zip archive to release + uses: svenstaro/upload-release-action@v2 + if: github.event.inputs.release_tag != '' + with: + file: ${{ github.workspace }}/v2rayN*.zip + tag: ${{ github.event.inputs.release_tag }} + file_glob: true + prerelease: true \ No newline at end of file diff --git a/.github/workflows/winget-publish.yml b/.github/workflows/winget-publish.yml new file mode 100644 index 00000000..c30b52d0 --- /dev/null +++ b/.github/workflows/winget-publish.yml @@ -0,0 +1,39 @@ +name: WinGet submission on release +# based off of https://github.com/nushell/nushell/blob/main/.github/workflows/winget-submission.yml +# inspired by https://github.com/microsoft/PowerToys/blob/main/.github/workflows/package-submissions.yml +# Modified by @MerrickZ https://github.com/anpho + +on: + workflow_dispatch: + release: + types: [released] + +jobs: + winget: + name: Publish winget package + runs-on: windows-latest + steps: + - name: Submit v2ray package to Windows Package Manager Community Repository + run: | + + $wingetPackage = "2dust.v2rayN" + $gitToken = "${{ secrets.PT_WINGET }}" + + $github = Invoke-RestMethod -uri "https://api.github.com/repos/2dust/v2rayN/releases" + + $targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1 + + $x64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-64\.zip' | Select -ExpandProperty browser_download_url + $arm64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-arm64\.zip' | Select -ExpandProperty browser_download_url + + $ver = $targetRelease.tag_name + + # getting latest wingetcreate file + iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe + + Write-Host "Updating with both x64 and arm64 installers" + Write-Host "Version: $ver" + Write-Host "x64 URL: $x64InstallerUrl" + Write-Host "arm64 URL: $arm64InstallerUrl" + + .\wingetcreate.exe update $wingetPackage -s -v $ver -u "$x64InstallerUrl|x64" "$arm64InstallerUrl|arm64" -t $gitToken diff --git a/.gitignore b/.gitignore index afeea2ed..7d5416b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,401 @@ -################################################################################ -# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。 -################################################################################ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore -/v2rayN/.vs/ -/v2rayN/v2rayN/bin/Debug/app.publish -/v2rayN/v2rayN/bin/Debug -/v2rayN/v2rayN/bin/Release -/v2rayN/v2rayN/obj/ -/v2rayN/.vs/v2rayN/DesignTimeBuild -/v2rayN/packages -.vs/ProjectSettings.json -.vs/slnx.sqlite -.vs/VSWorkspaceState.json -/v2rayN/v2rayUpgrade/bin/Debug -/v2rayN/v2rayUpgrade/obj/Debug -/v2rayN/v2rayUpgrade/bin/Release -/v2rayN/v2rayUpgrade/obj/Release \ No newline at end of file +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +.idea/ +*.sln.iml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..0ab249d5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "v2rayN/GlobalHotKeys"] + path = v2rayN/GlobalHotKeys + url = https://github.com/2dust/GlobalHotKeys diff --git a/LICENSE b/LICENSE index 94a9ed02..bfbc868f 100644 --- a/LICENSE +++ b/LICENSE @@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - Copyright (C) + Copyright (C) 2019-Present 2dust This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - Copyright (C) + v2rayN Copyright (C) 2019-Present 2dust This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. diff --git a/README.md b/README.md index c1197a8b..4d728c9b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ # v2rayN -### How to use -- If you are newbie please download v2rayN-Core.zip from releases -- Otherwise please download v2rayN.zip (Also need to download v2ray core in the same folder) -- Run v2rayN.exe +A GUI client for Windows, Linux and macOS, support [Xray](https://github.com/XTLS/Xray-core) +and [sing-box](https://github.com/SagerNet/sing-box) +and [others](https://github.com/2dust/v2rayN/wiki/List-of-supported-cores) -### Requirements -- Microsoft [.NET Framework 4.6](https://docs.microsoft.com/zh-cn/dotnet/framework/install/guide-for-developers) or higher -- Project V core [https://github.com/v2fly/v2ray-core/releases](https://github.com/v2fly/v2ray-core/releases) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayN)](https://github.com/2dust/v2rayN/commits/master) +[![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayn/badge)](https://www.codefactor.io/repository/github/2dust/v2rayn) +[![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayN/latest/total?logo=github)](https://github.com/2dust/v2rayN/releases) +[![Chat on Telegram](https://img.shields.io/badge/Chat%20on-Telegram-brightgreen.svg)](https://t.me/v2rayn) + +## How to use + +Read the [Wiki](https://github.com/2dust/v2rayN/wiki) for details. + +## Telegram Channel + +[github_2dust](https://t.me/github_2dust) diff --git a/package-appimage.sh b/package-appimage.sh new file mode 100644 index 00000000..6a8bfcca --- /dev/null +++ b/package-appimage.sh @@ -0,0 +1,67 @@ +#!/bin/bash +set -euo pipefail + +# Install deps +sudo apt update -y +sudo apt install -y libfuse2 wget file + +# Get tools +wget -qO appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage +chmod +x appimagetool + +# x86_64 AppDir +APPDIR_X64="AppDir-x86_64" +rm -rf "$APPDIR_X64" +mkdir -p "$APPDIR_X64/usr/lib/v2rayN" "$APPDIR_X64/usr/bin" "$APPDIR_X64/usr/share/applications" "$APPDIR_X64/usr/share/pixmaps" +cp -rf "$OutputPath64"/* "$APPDIR_X64/usr/lib/v2rayN" || true +[ -f "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_X64/usr/share/pixmaps/v2rayN.png" || true +[ -f "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_X64/v2rayN.png" || true + +printf '%s\n' '#!/bin/sh' 'HERE="$(dirname "$(readlink -f "$0")")"' 'cd "$HERE/usr/lib/v2rayN"' 'exec "$HERE/usr/lib/v2rayN/v2rayN" "$@"' > "$APPDIR_X64/AppRun" +chmod +x "$APPDIR_X64/AppRun" +ln -sf usr/lib/v2rayN/v2rayN "$APPDIR_X64/usr/bin/v2rayN" +cat > "$APPDIR_X64/v2rayN.desktop" < "$APPDIR_ARM64/AppRun" +chmod +x "$APPDIR_ARM64/AppRun" +ln -sf usr/lib/v2rayN/v2rayN "$APPDIR_ARM64/usr/bin/v2rayN" +cat > "$APPDIR_ARM64/v2rayN.desktop" < "${PackagePath}/opt/v2rayN/NotStoreConfigHere.txt" + +if [ $Arch = "linux-64" ]; then + Arch2="amd64" +else + Arch2="arm64" +fi +echo $Arch2 + +# basic +cat >"${PackagePath}/DEBIAN/control" <<-EOF +Package: v2rayN +Version: $Version +Architecture: $Arch2 +Maintainer: https://github.com/2dust/v2rayN +Depends: desktop-file-utils, xdg-utils +Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others +EOF + +cat >"${PackagePath}/DEBIAN/postinst" <<-EOF +if [ ! -s /usr/share/applications/v2rayN.desktop ]; then + cat >/usr/share/applications/v2rayN.desktop<<-END +[Desktop Entry] +Name=v2rayN +Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others +Exec=/opt/v2rayN/v2rayN +Icon=/opt/v2rayN/v2rayN.png +Terminal=false +Type=Application +Categories=Network;Application; +END +fi + +update-desktop-database +EOF + +sudo chmod 0755 "${PackagePath}/DEBIAN/postinst" +sudo chmod 0755 "${PackagePath}/opt/v2rayN/v2rayN" +sudo chmod 0755 "${PackagePath}/opt/v2rayN/AmazTool" + +# Patch +# set owner to root:root +sudo chown -R root:root "${PackagePath}" +# set all directories to 755 (readable & traversable by all users) +sudo find "${PackagePath}/opt/v2rayN" -type d -exec chmod 755 {} + +# set all regular files to 644 (readable by all users) +sudo find "${PackagePath}/opt/v2rayN" -type f -exec chmod 644 {} + +# ensure main binaries are 755 (executable by all users) +sudo chmod 755 "${PackagePath}/opt/v2rayN/v2rayN" 2>/dev/null || true +sudo chmod 755 "${PackagePath}/opt/v2rayN/AmazTool" 2>/dev/null || true + +# build deb package +sudo dpkg-deb -Zxz --build $PackagePath +sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb" diff --git a/package-osx.sh b/package-osx.sh new file mode 100755 index 00000000..042e29b7 --- /dev/null +++ b/package-osx.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +Arch="$1" +OutputPath="$2" +Version="$3" + +FileName="v2rayN-${Arch}.zip" +wget -nv -O $FileName "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/$FileName" +7z x $FileName +cp -rf v2rayN-${Arch}/* $OutputPath + +PackagePath="v2rayN-Package-${Arch}" +mkdir -p "$PackagePath/v2rayN.app/Contents/Resources" +cp -rf "$OutputPath" "$PackagePath/v2rayN.app/Contents/MacOS" +cp -f "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN.icns" "$PackagePath/v2rayN.app/Contents/Resources/AppIcon.icns" +echo "When this file exists, app will not store configs under this folder" > "$PackagePath/v2rayN.app/Contents/MacOS/NotStoreConfigHere.txt" +chmod +x "$PackagePath/v2rayN.app/Contents/MacOS/v2rayN" + +cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF + + + + + CFBundleDevelopmentRegion + English + CFBundleDisplayName + v2rayN + CFBundleExecutable + v2rayN + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon + CFBundleIdentifier + 2dust.v2rayN + CFBundleName + v2rayN + CFBundlePackageType + APPL + CFBundleShortVersionString + ${Version} + CSResourcesFileMapped + + NSHighResolutionCapable + + + +EOF + +create-dmg \ + --volname "v2rayN Installer" \ + --window-size 700 420 \ + --icon-size 100 \ + --icon "v2rayN.app" 160 185 \ + --hide-extension "v2rayN.app" \ + --app-drop-link 500 185 \ + "v2rayN-${Arch}.dmg" \ + "$PackagePath/v2rayN.app" \ No newline at end of file diff --git a/package-release-zip.sh b/package-release-zip.sh new file mode 100644 index 00000000..60804e69 --- /dev/null +++ b/package-release-zip.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +Arch="$1" +OutputPath="$2" + +OutputArch="v2rayN-${Arch}" +FileName="v2rayN-${Arch}.zip" + +wget -nv -O $FileName "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/$FileName" + +ZipPath64="./$OutputArch" +mkdir $ZipPath64 + +cp -rf $OutputPath "$ZipPath64/$OutputArch" +7z a -tZip $FileName "$ZipPath64/$OutputArch" -mx1 \ No newline at end of file diff --git a/package-rhel.sh b/package-rhel.sh new file mode 100644 index 00000000..ea537c62 --- /dev/null +++ b/package-rhel.sh @@ -0,0 +1,808 @@ +#!/usr/bin/env bash +set -euo pipefail + +# == Require Red Hat Enterprise Linux/FedoraLinux/RockyLinux/AlmaLinux/CentOS OR Ubuntu/Debian == +if [[ -r /etc/os-release ]]; then + . /etc/os-release + case "$ID" in + rhel|rocky|almalinux|fedora|centos|ubuntu|debian) + echo "[OK] Detected supported system: $NAME $VERSION_ID" + ;; + *) + echo "[ERROR] Unsupported system: $NAME ($ID)." + echo "This script only supports Red Hat Enterprise Linux/RockyLinux/AlmaLinux/CentOS or Ubuntu/Debian." + exit 1 + ;; + esac +else + echo "[ERROR] Cannot detect system (missing /etc/os-release)." + exit 1 +fi + +# ===== Config & Parse arguments ========================================================= +VERSION_ARG="${1:-}" # Pass version number like 7.13.8, or leave empty +WITH_CORE="both" # Default: bundle both xray+sing-box +AUTOSTART=0 # 1 = enable system-wide autostart (/etc/xdg/autostart) +FORCE_NETCORE=0 # --netcore => skip archive bundle, use separate downloads +ARCH_OVERRIDE="" # --arch x64|arm64|all (optional compile target) +BUILD_FROM="" # --buildfrom 1|2|3 to select channel non-interactively + +# If the first argument starts with --, do not treat it as a version number +if [[ "${VERSION_ARG:-}" == --* ]]; then + VERSION_ARG="" +fi +# Take the first non --* argument as version, discard it +if [[ -n "${VERSION_ARG:-}" ]]; then shift || true; fi + +# Parse remaining optional arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --with-core) WITH_CORE="${2:-both}"; shift 2;; + --autostart) AUTOSTART=1; shift;; + --xray-ver) XRAY_VER="${2:-}"; shift 2;; + --singbox-ver) SING_VER="${2:-}"; shift 2;; + --netcore) FORCE_NETCORE=1; shift;; + --arch) ARCH_OVERRIDE="${2:-}"; shift 2;; + --buildfrom) BUILD_FROM="${2:-}"; shift 2;; + *) + if [[ -z "${VERSION_ARG:-}" ]]; then VERSION_ARG="$1"; fi + shift;; + esac +done + +# Conflict: version number AND --buildfrom cannot be used together +if [[ -n "${VERSION_ARG:-}" && -n "${BUILD_FROM:-}" ]]; then + echo "[ERROR] You cannot specify both an explicit version and --buildfrom at the same time." + echo " Provide either a version (e.g. 7.14.0) OR --buildfrom 1|2|3." + exit 1 +fi + +# ===== Environment check + Dependencies ======================================== +host_arch="$(uname -m)" +[[ "$host_arch" == "aarch64" || "$host_arch" == "x86_64" ]] || { echo "Only supports aarch64 / x86_64"; exit 1; } + +install_ok=0 +case "$ID" in + # ------------------------------ RHEL family (UNCHANGED) ------------------------------ + rhel|rocky|almalinux|centos) + if command -v dnf >/dev/null 2>&1; then + sudo dnf -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync || \ + sudo dnf -y install dotnet-sdk rpm-build rpmdevtools curl unzip tar rsync + install_ok=1 + elif command -v yum >/dev/null 2>&1; then + sudo yum -y install dotnet-sdk-8.0 rpm-build rpmdevtools curl unzip tar rsync || \ + sudo yum -y install dotnet-sdk rpm-build rpmdevtools curl unzip tar rsync + install_ok=1 + fi + ;; + # ------------------------------ Ubuntu ---------------------------------------------- + ubuntu) + sudo apt-get update + # Ensure 'universe' (Ubuntu) to get 'rpm' + if ! apt-cache policy | grep -q '^500 .*ubuntu.com/ubuntu.* universe'; then + sudo apt-get -y install software-properties-common || true + sudo add-apt-repository -y universe || true + sudo apt-get update + fi + # Base tools + rpm (provides rpmbuild) + sudo apt-get -y install curl unzip tar rsync rpm || true + # Cross-arch binutils so strip matches target arch + objdump for brp scripts + sudo apt-get -y install binutils binutils-x86-64-linux-gnu binutils-aarch64-linux-gnu || true + # rpmbuild presence check + if ! command -v rpmbuild >/dev/null 2>&1; then + echo "[ERROR] 'rpmbuild' not found after installing 'rpm'." + echo " Please ensure the 'rpm' package is available from your repos (universe on Ubuntu)." + exit 1 + fi + # .NET SDK 8 (best effort via apt) + if ! command -v dotnet >/dev/null 2>&1; then + sudo apt-get -y install dotnet-sdk-8.0 || true + sudo apt-get -y install dotnet-sdk-8 || true + sudo apt-get -y install dotnet-sdk || true + fi + install_ok=1 + ;; + # ------------------------------ Debian (KEEP, with local dotnet install) ------------ + debian) + sudo apt-get update + # Base tools + rpm (provides rpmbuild on Debian) + objdump/strip + sudo apt-get -y install curl unzip tar rsync rpm binutils || true + # rpmbuild presence check + if ! command -v rpmbuild >/dev/null 2>&1; then + echo "[ERROR] 'rpmbuild' not found after installing 'rpm'." + echo " Please ensure 'rpm' is available from Debian repos." + exit 1 + fi + # Try apt for dotnet; fallback to official installer into $HOME/.dotnet + if ! command -v dotnet >/dev/null 2>&1; then + echo "[INFO] 'dotnet' not found. Installing .NET 8 SDK locally to \$HOME/.dotnet ..." + tmp="$(mktemp -d)"; trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN + curl -fsSL https://dot.net/v1/dotnet-install.sh -o "$tmp/dotnet-install.sh" + bash "$tmp/dotnet-install.sh" --channel 8.0 --install-dir "$HOME/.dotnet" + export PATH="$HOME/.dotnet:$HOME/.dotnet/tools:$PATH" + export DOTNET_ROOT="$HOME/.dotnet" + if ! command -v dotnet >/dev/null 2>&1; then + echo "[ERROR] dotnet installation failed." + exit 1 + fi + fi + install_ok=1 + ;; +esac + +if [[ "$install_ok" -ne 1 ]]; then + echo "[WARN] Could not auto-install dependencies for '$ID'. Make sure these are available:" + echo " dotnet-sdk 8.x, curl, unzip, tar, rsync, rpm, rpmdevtools, rpm-build (on RPM-based distros)" +fi + +command -v curl >/dev/null + +# Root directory = the script's location +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Git submodules (best effort) +if [[ -f .gitmodules ]]; then + git submodule sync --recursive || true + git submodule update --init --recursive || true +fi + +# ===== Locate project ================================================================ +PROJECT="v2rayN.Desktop/v2rayN.Desktop.csproj" +if [[ ! -f "$PROJECT" ]]; then + PROJECT="$(find . -maxdepth 3 -name 'v2rayN.Desktop.csproj' | head -n1 || true)" +fi +[[ -f "$PROJECT" ]] || { echo "v2rayN.Desktop.csproj not found"; exit 1; } + +# ===== Resolve GUI version & auto checkout ============================================ +VERSION="" + +choose_channel() { + # If --buildfrom provided, map it directly and skip interaction. + if [[ -n "${BUILD_FROM:-}" ]]; then + case "$BUILD_FROM" in + 1) echo "latest"; return 0;; + 2) echo "prerelease"; return 0;; + 3) echo "keep"; return 0;; + *) echo "[ERROR] Invalid --buildfrom value: ${BUILD_FROM}. Use 1|2|3." >&2; exit 1;; + esac + fi + + # Print menu to stderr and read from /dev/tty so stdout only carries the token. + local ch="latest" sel="" + if [[ -t 0 ]]; then + echo "[?] Choose v2rayN release channel:" >&2 + echo " 1) Latest (stable) [default]" >&2 + echo " 2) Pre-release (preview)" >&2 + echo " 3) Keep current (do nothing)" >&2 + printf "Enter 1, 2 or 3 [default 1]: " >&2 + if read -r sel /dev/null 2>&1; then + tag="$(printf '%s' "$json" \ + | jq -r '[.[] | select(.prerelease==true)][0].tag_name' 2>/dev/null \ + | sed 's/^v//')" || true + fi + + # 2) Fallback to sed/grep only + if [[ -z "${tag:-}" || "${tag:-}" == "null" ]]; then + tag="$(printf '%s' "$json" \ + | tr '\n' ' ' \ + | sed 's/},[[:space:]]*{/\n/g' \ + | grep -m1 -E '"prerelease"[[:space:]]*:[[:space:]]*true' \ + | grep -Eo '"tag_name"[[:space:]]*:[[:space:]]*"v?[^"]+"' \ + | head -n1 \ + | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v?([^"]+)".*/\1/')" || true + fi + + [[ -n "${tag:-}" && "${tag:-}" != "null" ]] || return 1 + printf '%s\n' "$tag" +} + +git_try_checkout() { + # Try a series of refs and checkout when found. + local want="$1" ref="" + if git rev-parse --git-dir >/dev/null 2>&1; then + git fetch --tags --force --prune --depth=1 || true + if git rev-parse "refs/tags/v${want}" >/dev/null 2>&1; then + ref="v${want}" + elif git rev-parse "refs/tags/${want}" >/dev/null 2>&1; then + ref="${want}" + elif git rev-parse --verify "${want}" >/dev/null 2>&1; then + ref="${want}" + fi + if [[ -n "$ref" ]]; then + echo "[OK] Found ref '${ref}', checking out..." + git checkout -f "${ref}" + if [[ -f .gitmodules ]]; then + git submodule sync --recursive || true + git submodule update --init --recursive || true + fi + return 0 + fi + fi + return 1 +} + +if git rev-parse --git-dir >/dev/null 2>&1; then + if [[ -n "${VERSION_ARG:-}" ]]; then + echo "[*] Trying to switch v2rayN repo to version: ${VERSION_ARG}" + if git_try_checkout "${VERSION_ARG#v}"; then + VERSION="${VERSION_ARG#v}" + else + echo "[WARN] Tag '${VERSION_ARG}' not found." + ch="$(choose_channel)" + if [[ "$ch" == "keep" ]]; then + echo "[*] Keep current repository state (no checkout)." + if git describe --tags --abbrev=0 >/dev/null 2>&1; then + VERSION="$(git describe --tags --abbrev=0)" + else + VERSION="0.0.0+git" + fi + VERSION="${VERSION#v}" + else + echo "[*] Resolving ${ch} tag from GitHub releases..." + tag="" + if [[ "$ch" == "prerelease" ]]; then + tag="$(get_latest_tag_prerelease || true)" + if [[ -z "$tag" ]]; then + echo "[WARN] Failed to resolve prerelease tag, falling back to latest." + tag="$(get_latest_tag_latest || true)" + fi + else + tag="$(get_latest_tag_latest || true)" + fi + [[ -n "$tag" ]] || { echo "[ERROR] Failed to resolve latest tag for channel '${ch}'."; exit 1; } + echo "[*] Latest tag for '${ch}': ${tag}" + git_try_checkout "$tag" || { echo "[ERROR] Failed to checkout '${tag}'."; exit 1; } + VERSION="${tag#v}" + fi + fi + else + ch="$(choose_channel)" + if [[ "$ch" == "keep" ]]; then + echo "[*] Keep current repository state (no checkout)." + if git describe --tags --abbrev=0 >/dev/null 2>&1; then + VERSION="$(git describe --tags --abbrev=0)" + else + VERSION="0.0.0+git" + fi + VERSION="${VERSION#v}" + else + echo "[*] Resolving ${ch} tag from GitHub releases..." + tag="" + if [[ "$ch" == "prerelease" ]]; then + tag="$(get_latest_tag_prerelease || true)" + if [[ -z "$tag" ]]; then + echo "[WARN] Failed to resolve prerelease tag, falling back to latest." + tag="$(get_latest_tag_latest || true)" + fi + else + tag="$(get_latest_tag_latest || true)" + fi + [[ -n "$tag" ]] || { echo "[ERROR] Failed to resolve latest tag for channel '${ch}'."; exit 1; } + echo "[*] Latest tag for '${ch}': ${tag}" + git_try_checkout "$tag" || { echo "[ERROR] Failed to checkout '${tag}'."; exit 1; } + VERSION="${tag#v}" + fi + fi +else + echo "[WARN] Current directory is not a git repo; cannot checkout version. Proceeding on current tree." + VERSION="${VERSION_ARG:-}" + if [[ -z "$VERSION" ]]; then + if git describe --tags --abbrev=0 >/dev/null 2>&1; then + VERSION="$(git describe --tags --abbrev=0)" + else + VERSION="0.0.0+git" + fi + fi + VERSION="${VERSION#v}" +fi +echo "[*] GUI version resolved as: ${VERSION}" + +# ===== Helpers for core/rules download (use RID_DIR for arch sync) ===================== +download_xray() { + # Download Xray core and install to outdir/xray + local outdir="$1" ver="${XRAY_VER:-}" url tmp zipname="xray.zip" + mkdir -p "$outdir" + if [[ -n "${XRAY_VER:-}" ]]; then ver="${XRAY_VER}"; fi + if [[ -z "$ver" ]]; then + ver="$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest \ + | grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true + fi + [[ -n "$ver" ]] || { echo "[xray] Failed to get version"; return 1; } + if [[ "$RID_DIR" == "linux-arm64" ]]; then + url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-arm64-v8a.zip" + else + url="https://github.com/XTLS/Xray-core/releases/download/v${ver}/Xray-linux-64.zip" + fi + echo "[+] Download xray: $url" + tmp="$(mktemp -d)"; trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN + curl -fL "$url" -o "$tmp/$zipname" + unzip -q "$tmp/$zipname" -d "$tmp" + install -Dm755 "$tmp/xray" "$outdir/xray" +} + +download_singbox() { + # Download sing-box core and install to outdir/sing-box + local outdir="$1" ver="${SING_VER:-}" url tmp tarname="singbox.tar.gz" bin + mkdir -p "$outdir" + if [[ -n "${SING_VER:-}" ]]; then ver="${SING_VER}"; fi + if [[ -z "$ver" ]]; then + ver="$(curl -fsSL https://api.github.com/repos/SagerNet/sing-box/releases/latest \ + | grep -Eo '"tag_name":\s*"v[^"]+"' | sed -E 's/.*"v([^"]+)".*/\1/' | head -n1)" || true + fi + [[ -n "$ver" ]] || { echo "[sing-box] Failed to get version"; return 1; } + if [[ "$RID_DIR" == "linux-arm64" ]]; then + url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-arm64.tar.gz" + else + url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-amd64.tar.gz" + fi + echo "[+] Download sing-box: $url" + tmp="$(mktemp -d)"; trap '[[ -n "${tmp:-}" ]] && rm -rf "$tmp"' RETURN + curl -fL "$url" -o "$tmp/$tarname" + tar -C "$tmp" -xzf "$tmp/$tarname" + bin="$(find "$tmp" -type f -name 'sing-box' | head -n1 || true)" + [[ -n "$bin" ]] || { echo "[!] sing-box unpack failed"; return 1; } + install -Dm755 "$bin" "$outdir/sing-box" +} + +# ---- NEW: download_mihomo (REQUIRED in --netcore mode) ---- +download_mihomo() { + # Download mihomo into outroot/bin/mihomo/mihomo + local outroot="$1" + local url="" + if [[ "$RID_DIR" == "linux-arm64" ]]; then + url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-arm64/bin/mihomo/mihomo" + else + url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-64/bin/mihomo/mihomo" + fi + echo "[+] Download mihomo: $url" + mkdir -p "$outroot/bin/mihomo" + curl -fL "$url" -o "$outroot/bin/mihomo/mihomo" + chmod +x "$outroot/bin/mihomo/mihomo" || true +} + +# Move geo files to a unified path: outroot/bin +unify_geo_layout() { + local outroot="$1" + mkdir -p "$outroot/bin" + local names=( \ + "geosite.dat" \ + "geoip.dat" \ + "geoip-only-cn-private.dat" \ + "Country.mmdb" \ + "geoip.metadb" \ + ) + for n in "${names[@]}"; do + # If file exists under bin/xray/, move it up to bin/ + if [[ -f "$outroot/bin/xray/$n" ]]; then + mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n" + fi + # If file already in bin/, leave it as-is + if [[ -f "$outroot/bin/$n" ]]; then + : + fi + done +} + +# Download geo/rule assets; then unify to bin/ +download_geo_assets() { + local outroot="$1" + local bin_dir="$outroot/bin" + local srss_dir="$bin_dir/srss" + mkdir -p "$bin_dir" "$srss_dir" + + echo "[+] Download Xray Geo to ${bin_dir}" + curl -fsSL -o "$bin_dir/geosite.dat" \ + "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geosite.dat" + curl -fsSL -o "$bin_dir/geoip.dat" \ + "https://github.com/Loyalsoldier/V2ray-rules-dat/releases/latest/download/geoip.dat" + curl -fsSL -o "$bin_dir/geoip-only-cn-private.dat" \ + "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/geoip-only-cn-private.dat" + curl -fsSL -o "$bin_dir/Country.mmdb" \ + "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb" + + echo "[+] Download sing-box rule DB & rule-sets" + curl -fsSL -o "$bin_dir/geoip.metadb" \ + "https://github.com/MetaCubeX/meta-rules-dat/releases/latest/download/geoip.metadb" || true + + for f in \ + geoip-private.srs geoip-cn.srs geoip-facebook.srs geoip-fastly.srs \ + geoip-google.srs geoip-netflix.srs geoip-telegram.srs geoip-twitter.srs; do + curl -fsSL -o "$srss_dir/$f" \ + "https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geoip/$f" || true + done + for f in \ + geosite-cn.srs geosite-gfw.srs geosite-greatfire.srs \ + geosite-geolocation-cn.srs geosite-category-ads-all.srs geosite-private.srs; do + curl -fsSL -o "$srss_dir/$f" \ + "https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true + done + + # Unify to bin/ + unify_geo_layout "$outroot" +} + +# Prefer the prebuilt v2rayN core bundle; then unify geo layout +download_v2rayn_bundle() { + local outroot="$1" + local url="" + if [[ "$RID_DIR" == "linux-arm64" ]]; then + url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-arm64.zip" + else + url="https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-linux-64.zip" + fi + echo "[+] Try v2rayN bundle archive: $url" + local tmp zipname + tmp="$(mktemp -d)"; zipname="$tmp/v2rayn.zip" + curl -fL "$url" -o "$zipname" || { echo "[!] Bundle download failed"; return 1; } + unzip -q "$zipname" -d "$tmp" || { echo "[!] Bundle unzip failed"; return 1; } + + if [[ -d "$tmp/bin" ]]; then + mkdir -p "$outroot/bin" + rsync -a "$tmp/bin/" "$outroot/bin/" + else + rsync -a "$tmp/" "$outroot/" + fi + + rm -f "$outroot/v2rayn.zip" 2>/dev/null || true + # keep mihomo + # find "$outroot" -type d -name "mihomo" -prune -exec rm -rf {} + 2>/dev/null || true + + local nested_dir + nested_dir="$(find "$outroot" -maxdepth 1 -type d -name 'v2rayN-linux-*' | head -n1 || true)" + if [[ -n "${nested_dir:-}" && -d "$nested_dir/bin" ]]; then + mkdir -p "$outroot/bin" + rsync -a "$nested_dir/bin/" "$outroot/bin/" + rm -rf "$nested_dir" + fi + + # Unify to bin/ + unify_geo_layout "$outroot" + + echo "[+] Bundle extracted to $outroot" +} + +# ===== Build results collection for --arch all ======================================== +BUILT_RPMS=() # Will collect absolute paths of built RPMs +BUILT_ALL=0 # Flag to know if we should print the final summary + +# ===== Build (single-arch) function ==================================================== +build_for_arch() { + # $1: target short arch: x64 | arm64 + local short="$1" + local rid rpm_target archdir + case "$short" in + x64) rid="linux-x64"; rpm_target="x86_64"; archdir="x86_64" ;; + arm64) rid="linux-arm64"; rpm_target="aarch64"; archdir="aarch64" ;; + *) echo "[ERROR] Unknown arch '$short' (use x64|arm64)"; return 1;; + esac + + echo "[*] Building for target: $short (RID=$rid, RPM --target $rpm_target)" + + # .NET publish (self-contained) for this RID + dotnet clean "$PROJECT" -c Release + rm -rf "$(dirname "$PROJECT")/bin/Release/net8.0" || true + + dotnet restore "$PROJECT" + dotnet publish "$PROJECT" \ + -c Release -r "$rid" \ + -p:PublishSingleFile=false \ + -p:SelfContained=true \ + -p:IncludeNativeLibrariesForSelfExtract=true + + # Per-arch variables (scoped) + local RID_DIR="$rid" + local PUBDIR + PUBDIR="$(dirname "$PROJECT")/bin/Release/net8.0/${RID_DIR}/publish" + [[ -d "$PUBDIR" ]] + + # Make RID_DIR visible to download helpers (they read this var) + export RID_DIR + + # Per-arch working area + local PKGROOT="v2rayN-publish" + local WORKDIR + WORKDIR="$(mktemp -d)" + trap '[[ -n "${WORKDIR:-}" ]] && rm -rf "$WORKDIR"' RETURN + + # rpmbuild topdir selection + local TOPDIR SPECDIR SOURCEDIR USE_TOPDIR_DEFINE + if [[ "$ID" =~ ^(rhel|rocky|almalinux|centos)$ ]]; then + rpmdev-setuptree + TOPDIR="${HOME}/rpmbuild" + SPECDIR="${TOPDIR}/SPECS" + SOURCEDIR="${TOPDIR}/SOURCES" + USE_TOPDIR_DEFINE=0 + else + TOPDIR="${WORKDIR}/rpmbuild" + SPECDIR="${TOPDIR}/SPECS}" + SOURCEDIR="${TOPDIR}/SOURCES" + mkdir -p "${SPECDIR}" "${SOURCEDIR}" "${TOPDIR}/BUILD" "${TOPDIR}/RPMS" "${TOPDIR}/SRPMS" + USE_TOPDIR_DEFINE=1 + fi + + # Stage publish content + mkdir -p "$WORKDIR/$PKGROOT" + cp -a "$PUBDIR/." "$WORKDIR/$PKGROOT/" + + # Optional icon + local ICON_CANDIDATE + ICON_CANDIDATE="$(dirname "$PROJECT")/../v2rayN.Desktop/v2rayN.png" + [[ -f "$ICON_CANDIDATE" ]] && cp "$ICON_CANDIDATE" "$WORKDIR/$PKGROOT/v2rayn.png" || true + + # Prepare bin structure + mkdir -p "$WORKDIR/$PKGROOT/bin/xray" "$WORKDIR/$PKGROOT/bin/sing_box" + + # Bundle / cores per-arch + if [[ "$FORCE_NETCORE" -eq 0 ]]; then + if download_v2rayn_bundle "$WORKDIR/$PKGROOT"; then + echo "[*] Using v2rayN bundle archive." + else + echo "[*] Bundle failed, fallback to separate core + rules." + if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then + download_xray "$WORKDIR/$PKGROOT/bin/xray" || echo "[!] xray download failed (skipped)" + fi + if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then + download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)" + fi + download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)" + fi + else + echo "[*] --netcore specified: use separate core + rules." + if [[ "$WITH_CORE" == "xray" || "$WITH_CORE" == "both" ]]; then + download_xray "$WORKDIR/$PKGROOT/bin/xray" || echo "[!] xray download failed (skipped)" + fi + if [[ "$WITH_CORE" == "sing-box" || "$WITH_CORE" == "both" ]]; then + download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)" + fi + download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)" + # ---- REQUIRED: always fetch mihomo in netcore mode, per-arch ---- + download_mihomo "$WORKDIR/$PKGROOT" || echo "[!] mihomo download failed (skipped)" + fi + + # Tarball + mkdir -p "$SOURCEDIR" + tar -C "$WORKDIR" -czf "$SOURCEDIR/$PKGROOT.tar.gz" "$PKGROOT" + + # SPEC + local SPECFILE="$SPECDIR/v2rayN.spec" + mkdir -p "$SPECDIR" + cat > "$SPECFILE" <<'SPEC' +%global debug_package %{nil} +%undefine _debuginfo_subpackages +%undefine _debugsource_packages +# Ignore outdated LTTng dependencies incorrectly reported by the .NET runtime (to avoid installation failures) +%global __requires_exclude ^liblttng-ust\.so\..*$ + +Name: v2rayN +Version: __VERSION__ +Release: 1%{?dist} +Summary: v2rayN (Avalonia) GUI client for Linux (x86_64/aarch64) +License: GPL-3.0-only +URL: https://github.com/2dust/v2rayN +BugURL: https://github.com/2dust/v2rayN/issues +ExclusiveArch: aarch64 x86_64 +Source0: __PKGROOT__.tar.gz + +# Runtime dependencies (Avalonia / X11 / Fonts / GL) +Requires: libX11, libXrandr, libXcursor, libXi, libXext, libxcb, libXrender, libXfixes, libXinerama, libxkbcommon +Requires: fontconfig, freetype, cairo, pango, mesa-libEGL, mesa-libGL, xdg-utils + +%description +v2rayN Linux for Red Hat Enterprise Linux +Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 / Shadowsocks / tuic / WireGuard +Support Red Hat Enterprise Linux / Fedora Linux / Rocky Linux / AlmaLinux / CentOS +For more information, Please visit our website +https://github.com/2dust/v2rayN + +%prep +%setup -q -n __PKGROOT__ + +%build +# no build + +%install +install -dm0755 %{buildroot}/opt/v2rayN +cp -a * %{buildroot}/opt/v2rayN/ + +# Launcher (prefer native ELF first, then DLL fallback) +install -dm0755 %{buildroot}%{_bindir} +cat > %{buildroot}%{_bindir}/v2rayn << 'EOF' +#!/usr/bin/bash +set -euo pipefail +DIR="/opt/v2rayN" + +# Prefer native apphost +if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi + +# DLL fallback +for dll in v2rayN.Desktop.dll v2rayN.dll; do + if [[ -f "$DIR/$dll" ]]; then exec /usr/bin/dotnet "$DIR/$dll" "$@"; fi +done + +echo "v2rayN launcher: no executable found in $DIR" >&2 +ls -l "$DIR" >&2 || true +exit 1 +EOF +chmod 0755 %{buildroot}%{_bindir}/v2rayn + +# Desktop file +install -dm0755 %{buildroot}%{_datadir}/applications +cat > %{buildroot}%{_datadir}/applications/v2rayn.desktop << 'EOF' +[Desktop Entry] +Type=Application +Name=v2rayN +Comment=v2rayN for Red Hat Enterprise Linux +Exec=v2rayn +Icon=v2rayn +Terminal=false +Categories=Network; +EOF + +# Icon +if [ -f "%{_builddir}/__PKGROOT__/v2rayn.png" ]; then + install -dm0755 %{buildroot}%{_datadir}/icons/hicolor/256x256/apps + install -m0644 %{_builddir}/__PKGROOT__/v2rayn.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png +fi + +%post +/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true +/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true + +%postun +/usr/bin/update-desktop-database %{_datadir}/applications >/dev/null 2>&1 || true +/usr/bin/gtk-update-icon-cache -f %{_datadir}/icons/hicolor >/dev/null 2>&1 || true + +%files +%{_bindir}/v2rayn +/opt/v2rayN +%{_datadir}/applications/v2rayn.desktop +%{_datadir}/icons/hicolor/256x256/apps/v2rayn.png +SPEC + + # Autostart injection (inside %install) and %files entry + if [[ "$AUTOSTART" -eq 1 ]]; then + awk ' + BEGIN{ins=0} + /^%post$/ && !ins { + print "# --- Autostart (.desktop) ---" + print "install -dm0755 %{buildroot}/etc/xdg/autostart" + print "cat > %{buildroot}/etc/xdg/autostart/v2rayn.desktop << '\''EOF'\''" + print "[Desktop Entry]" + print "Type=Application" + print "Name=v2rayN (Autostart)" + print "Exec=v2rayn" + print "X-GNOME-Autostart-enabled=true" + print "NoDisplay=false" + print "EOF" + ins=1 + } + {print} + ' "$SPECFILE" > "${SPECFILE}.tmp" && mv "${SPECFILE}.tmp" "$SPECFILE" + + awk ' + BEGIN{infiles=0; done=0} + /^%files$/ {infiles=1} + infiles && done==0 && $0 ~ /%{_datadir}\/icons\/hicolor\/256x256\/apps\/v2rayn\.png/ { + print + print "%config(noreplace) /etc/xdg/autostart/v2rayn.desktop" + done=1 + next + } + {print} + ' "$SPECFILE" > "${SPECFILE}.tmp" && mv "${SPECFILE}.tmp" "$SPECFILE" + fi + + # Replace placeholders + sed -i "s/__VERSION__/${VERSION}/g" "$SPECFILE" + sed -i "s/__PKGROOT__/${PKGROOT}/g" "$SPECFILE" + + # ----- Select proper 'strip' per target arch on Ubuntu only (cross-binutils) ----- + # NOTE: We define only __strip to point to the target-arch strip. + # DO NOT override __brp_strip (it must stay the brp script path). + local STRIP_ARGS=() + if [[ "$ID" == "ubuntu" ]]; then + local STRIP_BIN="" + if [[ "$short" == "x64" ]]; then + STRIP_BIN="/usr/bin/x86_64-linux-gnu-strip" + else + STRIP_BIN="/usr/bin/aarch64-linux-gnu-strip" + fi + if [[ -x "$STRIP_BIN" ]]; then + STRIP_ARGS=( --define "__strip $STRIP_BIN" ) + fi + fi + + # Build RPM for this arch (force rpm --target to match compile arch) + if [[ "$USE_TOPDIR_DEFINE" -eq 1 ]]; then + rpmbuild -ba "$SPECFILE" --define "_topdir $TOPDIR" --target "$rpm_target" "${STRIP_ARGS[@]}" + else + rpmbuild -ba "$SPECFILE" --target "$rpm_target" "${STRIP_ARGS[@]}" + fi + + # Copy temporary rpmbuild to ~/rpmbuild on Debian/Ubuntu path + if [[ "$USE_TOPDIR_DEFINE" -eq 1 ]]; then + mkdir -p "$HOME/rpmbuild" + rsync -a "$TOPDIR"/ "$HOME/rpmbuild"/ + TOPDIR="$HOME/rpmbuild" + fi + + echo "Build done for $short. RPM at:" + local f + for f in "${TOPDIR}/RPMS/${archdir}/v2rayN-${VERSION}-1"*.rpm; do + [[ -e "$f" ]] || continue + echo " $f" + BUILT_RPMS+=("$f") + done +} + +# ===== Arch selection and build orchestration ========================================= +case "${ARCH_OVERRIDE:-}" in + "") + # No --arch: use host architecture + if [[ "$host_arch" == "aarch64" ]]; then + build_for_arch arm64 + else + build_for_arch x64 + fi + ;; + x64|amd64) + build_for_arch x64 + ;; + arm64|aarch64) + build_for_arch arm64 + ;; + all) + BUILT_ALL=1 + # Build x64 and arm64 separately; each package contains its own arch-only binaries. + build_for_arch x64 + build_for_arch arm64 + ;; + *) + echo "[ERROR] Unknown --arch '${ARCH_OVERRIDE}'. Use x64|arm64|all." + exit 1 + ;; +esac + +# ===== Final summary if building both arches ========================================== +if [[ "$BUILT_ALL" -eq 1 ]]; then + echo "" + echo "================ Build Summary (both architectures) ================" + if [[ "${#BUILT_RPMS[@]}" -gt 0 ]]; then + for rp in "${BUILT_RPMS[@]}"; do + echo "$rp" + done + else + echo "[WARN] No RPMs detected in summary (check build logs above)." + fi + echo "===================================================================" +fi diff --git a/v2rayN/AmazTool/AmazTool.csproj b/v2rayN/AmazTool/AmazTool.csproj new file mode 100644 index 00000000..4657a889 --- /dev/null +++ b/v2rayN/AmazTool/AmazTool.csproj @@ -0,0 +1,20 @@ + + + + Exe + + + + + ResXFileCodeGenerator + Resource.Designer.cs + + + + True + True + Resource.resx + + + + \ No newline at end of file diff --git a/v2rayN/AmazTool/Program.cs b/v2rayN/AmazTool/Program.cs new file mode 100644 index 00000000..4df39682 --- /dev/null +++ b/v2rayN/AmazTool/Program.cs @@ -0,0 +1,87 @@ +namespace AmazTool; + +internal static class Program +{ + [STAThread] + private static void Main(string[] args) + { + try + { + // If no arguments are provided, display usage guidelines and exit + if (args.Length == 0) + { + ShowHelp(); + return; + } + + // Log all arguments for debugging purposes + foreach (var arg in args) + { + Console.WriteLine(arg); + } + + // Parse command based on first argument + switch (args[0].ToLowerInvariant()) + { + case "rebootas": + // Handle application restart + HandleRebootAsync(); + break; + + case "help": + case "--help": + case "-h": + case "/?": + // Display help information + ShowHelp(); + break; + + default: + // Default behavior: handle as upgrade data + // Maintain backward compatibility with existing usage pattern + var argData = Uri.UnescapeDataString(string.Join(" ", args)); + HandleUpgrade(argData); + break; + } + } + catch (Exception ex) + { + // Global exception handling + Console.WriteLine($"An error occurred: {ex.Message}"); + Console.WriteLine("Press any key to exit..."); + Console.ReadKey(); + } + } + + /// + /// Display help information and usage guidelines + /// + private static void ShowHelp() + { + Console.WriteLine(Resx.Resource.Guidelines); + Console.WriteLine("Available commands:"); + Console.WriteLine(" rebootas - Restart the application"); + Console.WriteLine(" help - Display this help information"); + Thread.Sleep(5000); + } + + /// + /// Handle application restart + /// + private static void HandleRebootAsync() + { + Console.WriteLine("Restarting application..."); + Thread.Sleep(1000); + Utils.StartV2RayN(); + } + + /// + /// Handle application upgrade with the provided data + /// + /// Data for the upgrade process + private static void HandleUpgrade(string upgradeData) + { + Console.WriteLine("Upgrading application..."); + UpgradeApp.Upgrade(upgradeData); + } +} diff --git a/v2rayN/AmazTool/Resx/Resource.Designer.cs b/v2rayN/AmazTool/Resx/Resource.Designer.cs new file mode 100644 index 00000000..2283298a --- /dev/null +++ b/v2rayN/AmazTool/Resx/Resource.Designer.cs @@ -0,0 +1,171 @@ +//------------------------------------------------------------------------------ +// +// 此代码由工具生成。 +// 运行时版本:4.0.30319.42000 +// +// 对此文件的更改可能会导致不正确的行为,并且如果 +// 重新生成代码,这些更改将会丢失。 +// +//------------------------------------------------------------------------------ + +namespace AmazTool.Resx { + using System; + + + /// + /// 一个强类型的资源类,用于查找本地化的字符串等。 + /// + // 此类是由 StronglyTypedResourceBuilder + // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 + // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen + // (以 /str 作为命令选项),或重新生成 VS 项目。 + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resource() { + } + + /// + /// 返回此类使用的缓存的 ResourceManager 实例。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AmazTool.Resx.Resource", typeof(Resource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// 重写当前线程的 CurrentUICulture 属性,对 + /// 使用此强类型资源类的所有资源查找执行重写。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// 查找类似 Failed to terminate the v2rayN. Close it manually, or the upgrade may fail. 的本地化字符串。 + /// + internal static string FailedTerminateProcess { + get { + return ResourceManager.GetString("FailedTerminateProcess", resourceCulture); + } + } + + /// + /// 查找类似 Failed to extract the update package. 的本地化字符串。 + /// + internal static string FailedUnzipping { + get { + return ResourceManager.GetString("FailedUnzipping", resourceCulture); + } + } + + /// + /// 查找类似 Upgrade failed. 的本地化字符串。 + /// + internal static string FailedUpgrade { + get { + return ResourceManager.GetString("FailedUpgrade", resourceCulture); + } + } + + /// + /// 查找类似 Please run it from the main application. 的本地化字符串。 + /// + internal static string Guidelines { + get { + return ResourceManager.GetString("Guidelines", resourceCulture); + } + } + + /// + /// 查找类似 Information 的本地化字符串。 + /// + internal static string Information { + get { + return ResourceManager.GetString("Information", resourceCulture); + } + } + + /// + /// 查找类似 In progress, please wait... 的本地化字符串。 + /// + internal static string InProgress { + get { + return ResourceManager.GetString("InProgress", resourceCulture); + } + } + + /// + /// 查找类似 Start v2rayN, please wait... 的本地化字符串。 + /// + internal static string Restartv2rayN { + get { + return ResourceManager.GetString("Restartv2rayN", resourceCulture); + } + } + + /// + /// 查找类似 Start extracting the update package... 的本地化字符串。 + /// + internal static string StartUnzipping { + get { + return ResourceManager.GetString("StartUnzipping", resourceCulture); + } + } + + /// + /// 查找类似 Successfully extracted the update package. 的本地化字符串。 + /// + internal static string SuccessUnzipping { + get { + return ResourceManager.GetString("SuccessUnzipping", resourceCulture); + } + } + + /// + /// 查找类似 Upgrade success. 的本地化字符串。 + /// + internal static string SuccessUpgrade { + get { + return ResourceManager.GetString("SuccessUpgrade", resourceCulture); + } + } + + /// + /// 查找类似 Try to terminate the v2rayN process... 的本地化字符串。 + /// + internal static string TryTerminateProcess { + get { + return ResourceManager.GetString("TryTerminateProcess", resourceCulture); + } + } + + /// + /// 查找类似 Upgrade failed, file not found. 的本地化字符串。 + /// + internal static string UpgradeFileNotFound { + get { + return ResourceManager.GetString("UpgradeFileNotFound", resourceCulture); + } + } + } +} diff --git a/v2rayN/v2rayN/Forms/AddServer6Form.zh-Hans.resx b/v2rayN/AmazTool/Resx/Resource.resx similarity index 80% rename from v2rayN/v2rayN/Forms/AddServer6Form.zh-Hans.resx rename to v2rayN/AmazTool/Resx/Resource.resx index 375c223d..c5b9c41c 100644 --- a/v2rayN/v2rayN/Forms/AddServer6Form.zh-Hans.resx +++ b/v2rayN/AmazTool/Resx/Resource.resx @@ -117,47 +117,40 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - 取消(&C) + + Start v2rayN, please wait... - - 服务器 + + Please run it from the main application. - - 域名(SNI) + + Upgrade failed, file not found. - - *手填,方便识别管理 + + In progress, please wait... - - - 83, 12 + + Try to terminate the v2rayN process... - - 别名(remarks) + + Failed to terminate the v2rayN. Close it manually, or the upgrade may fail. - - 29, 12 + + Start extracting the update package... - - 密码 + + Successfully extracted the update package. - - 65, 12 + + Failed to extract the update package. - - 服务器端口 + + Upgrade failed. - - 65, 12 + + Upgrade success. - - 服务器地址 - - - 确定(&O) - - - 编辑或添加[Trojan]服务器 + + Information \ No newline at end of file diff --git a/v2rayN/v2rayN/Forms/SubSettingControl.zh-Hans.resx b/v2rayN/AmazTool/Resx/Resource.zh-Hans.resx similarity index 81% rename from v2rayN/v2rayN/Forms/SubSettingControl.zh-Hans.resx rename to v2rayN/AmazTool/Resx/Resource.zh-Hans.resx index 95e5122f..9729d961 100644 --- a/v2rayN/v2rayN/Forms/SubSettingControl.zh-Hans.resx +++ b/v2rayN/AmazTool/Resx/Resource.zh-Hans.resx @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ارسال دسته ای آدرس اینترنتی اشتراک گذاری به کلیپ بورد با موفقیت. + + + لطفا ابتدا تنظیمات سرور را بررسی کنید + + + فرمت پیکربندی نادرست است + + + توجه داشته باشید که پیکربندی سفارشی کاملاً به پیکربندی خود شما بستگی دارد و با همه تنظیمات کار نمی کند. اگر می خواهید از پروکسی سیستم استفاده کنید، لطفاً پورت درحال شنود را به صورت دستی تغییر دهید. + + + درحال دانلود... + + + تبدیل فایل پیکربندی انجام نشد + + + فایل پیکربندی پیش فرض ایجاد نشد + + + پیکربندی پیش فرض دریافت نشد + + + سرور پیکربندی سفارشی وارد نشد + + + فایل پیکربندی خوانده نشد + + + لطفا فرمت صحیح پورت سرور را وارد کنید. + + + لطفاً پورت گوش دادن محلی را وارد کنید. + + + لطفا رمز عبور را وارد کنید. + + + لطفا آدرس سرور را وارد کنید. + + + لطفا شناسه کاربری را وارد کنید. + + + این پیکربندی صحیحی نیست، لطفا بررسی کنید. + + + پیکربندی اولیه + + + {0} {1} در حال حاضر به روز است. + + + {0} {1} در حال حاضر به روز است. + + + آدرس + + + امنیت + + + پورت + + + نوع + + + گروه فرعی + + + ترافیک دانلود امروز + + + ترافیک اپلود امروز + + + کل ترافیک دانلود + + + کل ترافیک آپلود + + + جابجایی + + + هسته با موفقیت دانلود شد + + + محتوای اشتراک با موفقیت وارد نشد + + + محتوای اشتراک با موفقیت دریافت شد + + + هیچ اشتراک معتبری تنظیم نشده است + + + {0} با موفقیت حل شد + + + شروع به دریافت اشتراک کرد + + + شروع به‌روزرسانی {0}... + + + محتوای اشتراک نامعتبر است + + + باز کردن بسته بندی... + + + بروزرسانی اشتراک به پایان رسید + + + بروزرسانی اشتراک شروع شد + + + هسته با موفقیت بروزرسانی شد + + + هسته با موفقیت بروزرسانی شد! راه اندازی مجدد سرویس... + + + پروتکل غیر VMess یا ss + + + فایل Core (نام فایل: {1}) در زیر پوشه ({0}) یافت نشد، لطفاً آن را دانلود کرده و در پوشه قرار دهید، آدرس دانلود: {2} + + + اسکن کامل شد، QRcode معتبری یافت نشد + + + عملیات انجام نشد، لطفا بررسی کنید و دوباره امتحان کنید + + + لطفا ملاحظات را پر کنید + + + لطفاً روش رمزگذاری را انتخاب کنید + + + لطفا یک پروتکل انتخاب کنید + + + لطفا ابتدا سرور را انتخاب کنید + + + حذف مجدد سرورها تکمیل شد. قدیمی: {0}، جدید: {1}. + + + آیا مطمئن هستید که می خواهید سرور را حذف کنید؟ + + + فایل پیکربندی کلاینت در این آدرس ذخیره می شود: {0} + + + شروع سرویس ({0})... + + + پیکربندی با موفقیت انجام شد{0} + + + سرور پیکربندی سفارشی با موفقیت وارد شد. + + + {0} سرورها از کلیپ بورد وارد شده اند. + + + آدرس اینترنتی با موفقیت اسکن و وارد شد + + + پینگ سرویس فعلی: {0} ms, {1} + + + عملیات با موفقیت انجام شد + + + لطفا قوانین را انتخاب کنید + + + آیا مطمئن هستید که می خواهید قوانین را حذف کنید؟ + + + {0}، یکی از فیلدهای الزامی است. + + + ملاحظات + + + آدرس اینترنتی (اختیاری) + + + شمارش + + + لطفا آدرس (آدرس اینترنتی) را وارد کنید + + + آآیا می خواهید قوانین را اضافه کنید؟ بله برای افزودن، نه برای جایگزینی را انتخاب کنید. + + + GeoFile بارگیری شد: {0} با موفقیت + + + اطلاعات + + + نماد سفارشی + + + لطفاً DNS سفارشی صحیح را پر کنید + + + * مسیر ws + + + * مسیر h2 + + + *QUIC key/Kcp seed + + + *grpc serviceName + + + *هاست http جدا شده با کاما (،) + + + *هاست ws + + + *هاست h2 با کاما (،) جدا شده است + + + *QUIC securty + + + *tcp camouflage type + + + *kcp camouflage type + + + *QUIC camouflage type + + + *حالت grpc + + + TLS + + + *Kcp seed + + + ثبت کلید جهانی {0} انجام نشد، دلیل: {1} + + + کلید میانبر جهانی {0} با موفقیت ثبت شد + + + همه سرورها + + + لطفاً برای وارد کردن پیکربندی سرور مرور کنید + + + درحال تست کردن... + + + شبکه محلی + + + محلی + + + فیلتر سرورها + + + بررسی بروزرسانی + + + بستن + + + خروج + + + تنظیم کلید میانبر کلی + + + کمک + + + تنظیمات پارامتر + + + ترفیع + + + بارگذاری مجدد + + + تنظیم مسیریابی + + + سرورها + + + تنظیمات + + + اشتراک فعلی را بدون پروکسی به روز شود + + + اشتراک فعلی را با پروکسی به روز شود + + + گروه اشتراک + + + تنظیم گروه اشتراک + + + اشتراک را بدون پروکسی به روز شود + + + اشتراک را با پروکسی به روز شود + + + پروکسی سیستم + + + پاک کردن پروکسی سیستم + + + پروکسی سیستم تغییر نکند + + + حالت Pac + + + تنظیم پراکسی سیستم + + + رنگ + + + زبان + + + وارد کردن URL انبوه از کلیپ بورد (Ctrl+V) + + + اسکن کد QR روی صفحه (Ctrl+S) + + + سرور انتخاب شده را شبیه سازی کنید + + + سرورهای تکراری را حذف کنید + + + حذف سرورهای انتخابی (Delete) + + + به عنوان سرور فعال تنظیم کنید (Enter) + + + تمام آمار خدمات را پاک کنید + + + آزمایش سرورها با تاخیر واقعی (Ctrl+R) + + + مرتب سازی بر اساس نتیجه تست + + + تست سرعت دانلود سرورها (Ctrl+T) + + + تست سرورها با tcping (Ctrl+O) + + + سرور انتخابی را برای پیکربندی کلاینت صادر کنید + + + URL های اشتراک گذاری را به کلیپ بورد صادر کنید (Ctrl+C) + + + یک سرور پیکربندی سفارشی اضافه شود + + + سرور [شادوساکس] را اضافه کنید + + + سرور [ساکس] را اضافه کنید + + + سرور [تروجان] را اضافه کنید + + + سرور [VLESS] را اضافه کنید + + + سرور [VMess] را اضافه کنید + + + انتخاب همه (Ctrl+A) + + + همه را پاک کن + + + کپی (Ctrl+C) + + + کپی همه + + + انتخاب همه (Ctrl+A) + + + اضافه کردن + + + حذف + + + ویرایش + + + اشتراک + + + به روز رسانی فعال شد + + + مرتب سازی + + + عامل کاربر + + + لغو + + + تایید + + + انتقال + + + آدرس + + + اعطای مجوز ناامن + + + Alpn + + + AlterId + + + اثرانگشت + + + نوع Camouflage + + + UUID(id) + + + پروتکل جابجایی (شبکه) + + + مسیر + + + پورت + + + نام مستعار (ملاحظات) + + + Camouflage domain(host) + + + روش رمزگذاری (امنیتی) + + + SNI + + + TLS + + + *مقدار پیش فرض tcp + + + نوع هسته + + + جریان + + + ساختن + + + رمزعبور + + + رمز عبور (اختیاری) + + + UUID(id) + + + رمزگذاری + + + کاربر (اختیاری) + + + رمزگذاری + + + txtPreSocksPort + + + * پس از تنظیم این مقدار، یک سرویس جوراب با استفاده از Xray/sing-box (Tun) برای ارائه عملکردهایی مانند نمایش سرعت شروع می شود. + + + مرور کردن + + + ویرایش + + + تنظیمات پیشرفته پروکسی، انتخاب پروتکل (اختیاری) + + + اتصالات از LAN را مجاز کنید + + + مخفی کردن خودکار هنگام راه اندازی + + + فاصله به روز رسانی خودکار برای فایل های Geo (ساعت) + + + هسته: تنظیمات اولیه + + + تنظیمات V2ray DNS + + + هسته: تنظیمات KCP + + + تنظیمات CoreType + + + اعطای مجوز ناامن + + + Outbound Freedom domainStrategy + + + پس از به‌روزرسانی اشتراک، عرض ستون به صورت خودکار تنظیم شود + + + به روز رسانی های پیش از انتشار را بررسی شود + + + استثنا + + + استثناها: از سرور پروکسی برای آدرس هایی که با موارد زیر شروع می شوند استفاده نکنید. برای جدا کردن ورودی ها از نقطه ویرگول (;) استفاده کنید. + + + نمایش سرعت واقعی (نیاز به راه اندازی مجدد) + + + ورودی‌های قدیمی‌تر را هنگام حذف کپی نگه دارید + + + ثبت گزارش های محلی + + + سطح ثبت رویداد + + + فعال کردن Mux Multiplexing + + + تنظیمات v2rayN + + + مجوز احراز هویت + + + سفارشی DNS (multiple, separated by commas (,)) + + + تنظیم کردن Win10 UWP Loopback + + + فعال کردن Sniffing + + + پورت Mixed + + + درهنگام راه ائدازی شروع شود + + + فعال کردن آمار ترافیک (نیاز به راه اندازی مجدد) + + + آدرس اینترنتی تبدیل اشتراک + + + تنظیمات پراکسی سیستم + + + فعال کردن پروتکل امنیتی TLS نسخه 1.3 (اشتراک/به‌روزرسانی) + + + محدودیت نمایش سرورهای منوی سینی کلیک راست + + + فعال سازی UDP + + + تایید کاربر + + + پاک کردن پروکسی سیستم + + + نمایش رابط کاربری گرافیکی + + + تنظیم کلید میانبر جهانی + + + مستقیماً با فشار دادن صفحه کلید تنظیم کنید. پس از راه اندازی مجدد اعمال می شود + + + پروکسی سیستم را تغییر ندهید + + + بازنشانی + + + تنظیم پراکسی سیستم + + + حالت Pac + + + اشتراک گذاری سرور(Ctrl+F) + + + مسیریابی + + + به عنوان ادمین اجرا نمی شود + + + اجرا به عنوان ادمین + + + به پایین حرکت شود(B) + + + پایین (D) + + + حرکت به بالا (T) + + + بالا (U) + + + فیلتر، از عبارات منظم پشتیبانی می کند + + + {0} وب سایت + + + اضافه کردن + + + وارد کردن مجموعه قوانین + + + حذف انتخاب شده + + + تنظیم کردن به عنوان قانون فعال + + + استراتژی دامنه + + + لیست مجموعه قوانین از پیش تعریف شده + + + *قوانین را با کاما (,) جدا کنید. برای کاما به معنای واقعی کلمه از <COMMA>; پیشوند # برای نادیده گرفتن یک قانون + + + وارد کردن قوانین از کلیپ بورد + + + وارد کردن قوانین از فایل + + + وارد کردن قوانین از آدرس اینترنتی Sub + + + تنظیم قانون + + + اضافه کردن قانون + + + صادر کردن قوانین انتخاب شده + + + فهرست قوانین + + + حذف قوانین + + + تنظیم جزئیات قانون مسیریابی + + + دامنه و آی پی در هنگام ذخیره به طور خودکار مرتب می شوند + + + مستندات شی قانون + + + پشتیبانی از DnsObject. برای مشاهده مستندات کلیک کنید + + + گروه لطفا اینجا را خالی بگذارید + + + تنظیمات مسیریابی تغییر کرده است + + + تنظیمات پراکسی سیستم تغییر کرده است + + + فقط مسیر + + + از سرورهای پروکسی برای آدرس های محلی (اینترانت) استفاده نکنید + + + تاخیر و سرعت تست با یک کلیک (Ctrl+E) + + + تاخیر (میلی‌ثانیه) + + + سرعت (M/s) + + + Core اجرا نشد، لطفاً گزارش را ببینید + + + Remarks regular filter + + + نمایش گزارش + + + فعال سازی Tun + + + پورت جدید برای LAN + + + تنظیمات TunMode + + + انتقال به گروه + + + فعال کردن مرتب‌سازی سرورها با کشیدن و رها کردن (نیاز به راه‌اندازی مجدد) + + + بازخوانی خودکار + + + رد شدن از آزمون + + + ویرایش سرور (Ctrl+D) + + + دوبار کلیک کردن سرور باعث فعال شدن آن می شود + + + تست تکمیل شد + + + اثر انگشت tls پیش فرض + + + User-Agent + + + این پارامتر فقط برای tcp/http و ws معتبر است + + + FontFamily (نیاز به راه اندازی مجدد) + + + فایل TTF/TTC فونت را در دایرکتوری guiFonts کپی کنید. پنجره تنظیمات را دوباره باز کنید + + + پورت Pac = +3; پورت Xray API = +4; پورت mihomo API = +5; + + + این را با امتیازات ادمین تنظیم کنید، پس از راه اندازی، امتیازات مدیر را دریافت کنید + + + اندازه فونت + + + یمقدار تاخیر تست سرعت منفرد + + + /آدرس اینترنتی SpeedTest + + + بالا و پایین حرکت کنید + + + PublicKey + + + ShortId + + + SpiderX + + + فعال‌ سازی شتاب‌ دهنده سخت‌ افزاری (نیاز به راه‌اندازی مجدد) + + + در انتظار آزمایش (برای پایان دادن به ESC فشار دهید)... + + + لطفاً در صورت قطع غیرعادی آن را خاموش کنید + + + به روز رسانی ها فعال نیستند، از این اشتراک رد شوید + + + به عنوان مدیر راه اندازی مجدد + + + نشانی‌های وب بیشتر که با کاما از هم جدا شده‌اند. تبدیل اشتراک نامعتبر خواهد بود + + + {0} : {1}/s↑ | {2}/s↓ + + + فاصله به روز رسانی خودکار (دقیقه) + + + فعال کردن ورود به فایل + + + تبدیل نوع هدف + + + اگر نیازی به تبدیل نیست، لطفاً خالی بگذارید + + + تنظیمات DNS + + + تنظیمات DNS sing-box + + + لطفا ساختار DNS را پر کنید، برای مشاهده سند کلیک کنید + + + برای وارد کردن تنظیمات پیش‌فرض DNS کلیک کنید + + + استراتژی دامنه sing-box + + + پروتکل sing-box Mux + + + نام کامل فرانید (حالت Tun) + + + IP or IP CIDR + + + دامنه + + + Add [Hysteria2] server + + + حداکتر پهنای باند هیستریا (آپلود/دانلود) + + + استفاده کردن از System Hosts + + + افزودن سرور [TUIC] + + + کنترل تراکم + + + نام مستعار پروکسی قبلی + + + نام مستعار پروکسی بعدی + + + لطفاً مطمئن شوید که ملاحظات وجود دارند و منحصر به فرد هستند + + + مسیریابی خودکار + + + مسیریابی سخت‌گیرانه + + + پشته شبکه + + + MTU + + + فعال سازی additional Inbound + + + فعال سازی آدرس IPv6 + + + افزودن سرور [WireGuard] + + + کلید خصوصی + + + Reserved (2,3,4) + + + آدرس (IPv4, IPv6) + + + پسورد obfs + + + (Domain or IP or ProcName) and Port and Protocol and InboundTag => OutboundTag + + + خودکار ScrollToEnd + + + آدرس اینترنتی تست پینگ سرعت + + + اشتراک در حال به‌روزرسانی، فقط مشخص کنید که ملاحظاتی آیا وجود دارد! + + + پایان تست... + + + *grpc Authority + + + افزودن سرور [HTTP] + + + which conflicts with the group previous proxy + + + فعال کردن فرگمنت + + + فعال کردن کش فایل مجموعه قوانین برای sing-box + + + سفارش سازی مجموعه قوانین sing box + + + عملکرد موفقیت آمیز بود، روی منوی تنظیمات کلیک کنید تا برنامه راه اندازی مجدد شود. + + + باز کردن محل ذخیره سازی + + + مرتب سازی + + + Chain + + + پیش فرض + + + تاخیر + + + سرعت دانلود + + + ترافیک دانلود + + + هاست + + + نام + + + شبکه + + + زمان + + + نوع + + + سرعت اپلود + + + ترافیک آپلود + + + اتصالات + + + بستن اتصال + + + تمام اتصالات را ببندید + + + پروکسی + + + نوع قانون + + + مستقیم + + + جهانی + + + تغییر نده + + + قانون + + + تست تأخیر + + + تست تاخیر قسمت گره (نقطه اتصال) + + + تازه سازی پروکسی ها + + + انتخاب گره فعال (Enter) + + + استراتژی دامنه پیش فرض برای خروجی + + + جهت چیدمان اصلی (نیاز به راه اندازی مجدد) + + + آدرس DNS خروجی + + + تنظیم خودکار عرض ستون + + + صادر کردن پیوندهای اشتراک گذاری کدگذاری شده با Base64 به کلیپ بورد + + + صادر کردن سرور انتخاب شده برای پیکربندی کامل به کلیپ بورد + + + نمایش یا پنهان کردن پنجره اصلی + + + پیکربندی سفارشی ساکس پورت + + + پشتیبان گیری و بازیابی + + + پشتیبان گیری به محلی + + + بازیابی از محلی + + + پشتیبان گیری از راه دور (WebDAV) + + + بازیابی از راه دور (WebDAV) + + + محلی + + + از راه دور (WebDAV) + + + آدرس اینترنتی WebDav + + + نام کاربری WebDav + + + پسورد WebDav + + + چک کردن WebDav + + + نام پوشه راه دور (اختیاری) + + + فایل پشتیبان نامعتبر است + + + فیلتر هسته + + + فعال سازی + + + منبع فایل های جغرافیایی (اختیاری) + + + منبع فایل های مجموعه قوانین sing-box (اختیاری) + + + برنامه ارتقا وجود ندارد + + + منبع قوانین مسیریابی (اختیاری) + + + تنظیمات از پیش تعیین شده منطقه ای + + + پیش فرض + + + روسیه + + + ایران + + + کاربران در منطقه چین می توانند این مورد را نادیده بگیرند + + + اسکن کردن QRcode موجود در تصویر + + + آدرس نامعتبر (آدرس اینترنتی) + + + لطفاً از آدرس اشتراک پروتکل HTTP ناامن استفاده نکنید + + + فونت را روی سیستم نصب کنید و تنظیمات را مجددا راه اندازی کنید + + + آیا مطمئن هستید که خارج می شوید؟ + + + یادداشت ملاحظات + + + رمز عبور sudo سیستم + + + The password will be validated via the command line. If a validation error causes the application to malfunction, please restart the application. The password will not be stored and must be entered again after each restart. + + + *حالت xhttp + + + جیسون خام XHTTP Extra, فرمت: { XHTTPObject } + + + هنگام بستن پنجره در سینی پنهان شوید + + + تعداد همزمان در طول چند آزمون + + + موارد استثنا: از سرور پروکسی برای آدرس های زیر استفاده نکنید. برای جدا کردن ورودی ها از کاما (،) استفاده کنید. + + + نوع Sniffing + + + فعال کردن دومین پورت ترکیبی + + + socks:پورت محلی، socks2: پورت دوم محلی، socks3: پورت LAN + + + تم + + + کپی کردن دستور پروکسی در کلیپ بورد + + + شروع آزمایش مجدد قطعات ناموفق، {0} باقی مانده است. برای خاتمه ESC را فشار دهید... + + + با نتیجه آزمایش + + + حذف نامعتبر با نتایج آزمایش + + + {0} نتایج آزمایش نامعتبر حذف شد. + + + محدوده پورت سرور + + + مخفی و پورت می شود، با کاما (،) جدا می شود + + + چند سرور به پیکربندی سفارشی + + + چند سرور تصادفی توسط Xray + + + چند سرور RoundRobin توسط Xray + + + چند سرور LeastPing توسط Xray + + + چند سرور LeastLoad توسط Xray + + + LeastPing چند سرور توسط sing-box + + + صادر کردن سرور + + + URL آزمایش اطلاعات اتصال فعلی + + + Can fill in the configuration remarks, please make sure it exist and are unique + + + Incorrect password, please try again. + + + Mldsa65Verify + + + Add [Anytls] Configuration + + + Remote DNS + + + Domestic DNS + + + Outbound DNS Resolution (sing-box) + + + Resolve Outbound Domains + + + sing-box DoH Resolver Server + + + Fallback DNS Resolution, Suggest IP + + + xray Freedom Resolution Strategy + + + sing-box Direct Resolution Strategy + + + sing-box Remote Resolution Strategy + + + Add Common DNS Hosts + + + The sing-box DoH resolution server can be overwritten + + + FakeIP + + + Block SVCB and HTTPS Queries + + + DNS Hosts: ("domain1 ip1 ip2" per line) + + + Apply to Proxy Domains Only + + + Basic DNS Settings + + + Advanced DNS Settings + + + Validate Regional Domain IPs + + + When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs + + + Enable Custom DNS + + + Custom DNS Enabled, This Page's Settings Invalid + + + Prevent domain-based routing rules from failing + + + Please fill in the correct config template + + + Full Config Template Setting + + + Enable Full Config Template + + + v2ray Full Config Template + + + Add Outbound Config Only, routing.balancers and routing.rules.outboundTag, Click to view the document + + + Do Not Add Non-Proxy Protocol Outbound + + + Set Upstream Proxy Tag + + + sing-box Full Config Template + + + Add Outbound and Endpoint Config Only, Click to view the document + + + This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you. + + + Start parsing and processing subscription content + + + Select Profile + + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx new file mode 100644 index 00000000..800fa00f --- /dev/null +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -0,0 +1,1518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Sikeresen exportálta a megosztási linket a vágólapra + + + Kérjük, először ellenőrizze a konfigurációs beállításokat. + + + Érvénytelen konfigurációs formátum. + + + Ne feledje, hogy az egyéni konfiguráció teljes mértékben a saját konfigurációjától függ, és nem működik minden beállítással. Ha a rendszerproxyt szeretné használni, kérjük, manuálisan módosítsa a figyelő portot. + + + Letöltés... + + + Nem sikerült a konfigurációs fájl konvertálása + + + Nem sikerült az alapértelmezett konfigurációs fájl generálása + + + Nem sikerült lekérni az alapértelmezett konfigurációt + + + Nem sikerült importálni az egyéni konfigurációt + + + Nem sikerült olvasni a konfigurációs fájlt + + + Kérjük, adja meg a helyes port formátumot. + + + Kérjük, adja meg a helyi figyelő portot. + + + Kérjük, adja meg a jelszót. + + + Kérjük, adja meg a címet. + + + Kérjük, adja meg a felhasználói azonosítót. + + + Ez nem a megfelelő konfiguráció, kérjük, ellenőrizze + + + Kezdeti konfiguráció + + + {0} {1} már naprakész. + + + {0} {1} már naprakész. + + + Cím + + + Biztonság + + + Port + + + Típus + + + Előfizetés csoport + + + Mai letöltési forgalom + + + Mai feltöltési forgalom + + + Összes letöltési forgalom + + + Összes feltöltési forgalom + + + Szállítás + + + A Core sikeresen letöltve + + + Nem sikerült importálni az előfizetés tartalmát + + + Az előfizetés tartalma sikeresen lekérve + + + Nincs érvényes előfizetés beállítva + + + Resolved {0} successfully + + + Előfizetések lekérdezése elindult + + + Frissítés indítása: {0}... + + + Érvénytelen előfizetés tartalom + + + Kicsomagolás... + + + Előfizetés frissítése befejeződött + + + Előfizetés frissítése elindult + + + A Core sikeresen frissítve + + + A Core sikeresen frissítve! Szolgáltatás újraindítása... + + + Nem VMess vagy SS protokoll + + + A Core fájl (fájlnév: {1}) nem található a mappában ({0}), kérjük, töltse le és helyezze a mappába, letöltési cím: {2} + + + Szkennelés befejeződött, nem található érvényes QR kód + + + Művelet sikertelen, kérjük, ellenőrizze és próbálja újra + + + Kérjük, töltse ki a megjegyzéseket + + + Kérjük, válassza ki a titkosítási módszert + + + Kérjük, válassza ki a protokollt + + + Kérjük, először válassza ki a konfigurációt + + + Konfigurációk deduplikálása befejeződött. Régi: {0}, Új: {1}. + + + Biztosan eltávolítja a konfigurációt? + + + Az ügyfélkonfigurációs fájl mentése itt: {0} + + + Szolgáltatás indítása ({0})... + + + Konfiguráció sikeres. {0} + + + Egyéni konfiguráció sikeresen importálva + + + {0} konfiguráció importálva a vágólapról + + + Sikeresen beolvasta és importálta a megosztott linket + + + A késleltetés: {0} ms, {1} + + + Művelet sikeres + + + Kérjük, válasszon szabályokat + + + Biztosan eltávolítja a szabályokat? + + + {0}, az egyik kötelező mező. + + + Megjegyzések + + + URL (opcionális) + + + Darabszám + + + Kérjük, adja meg az URL-t + + + Hozzá szeretne fűzni szabályokat? Igen a hozzáfűzéshez, nem a cseréhez. + + + A GeoFile: {0} sikeresen letöltve + + + Információ + + + Egyéni ikon + + + Kérjük, töltse ki a helyes egyéni DNS-t + + + *ws/http upgrade/xhttp elérési út + + + *h2 elérési út + + + *QUIC kulcs/KCP seed + + + *grpc szolgáltatásnév + + + *http host vesszővel elválasztva (,) + + + *ws/http upgrade/xhttp host + + + *h2 host vesszővel elválasztva (,) + + + *QUIC biztonság + + + *tcp álcázási típus + + + *kcp álcázási típus + + + *QUIC álcázási típus + + + *grpc mód + + + TLS + + + *kcp seed + + + Globális gyorsbillentyű {0} regisztrációja sikertelen, ok: {1} + + + Globális gyorsbillentyű {0} sikeresen regisztrálva + + + Összes + + + Kérjük, tallózzon a konfiguráció importálásához + + + Tesztelés... + + + LAN + + + Helyi + + + Konfigurációs szűrő, Enter billentyűvel végrehajtható + + + Frissítés ellenőrzése + + + Bezárás + + + Kilépés + + + Globális gyorsbillentyű beállítás + + + Súgó + + + Opció beállítás + + + Promóció + + + Újratöltés + + + Útválasztási beállítás + + + Konfigurációk + + + Beállítások + + + Aktuális előfizetés frissítése proxy nélkül + + + Aktuális előfizetés frissítése proxyval + + + Előfizetési csoport + + + Előfizetési csoport beállításai + + + Előfizetések frissítése proxy nélkül + + + Előfizetések frissítése proxyval + + + Rendszerproxy + + + Rendszerproxy törlése + + + Ne változtassa meg a rendszerproxyt + + + PAC mód + + + Rendszerproxy beállítása + + + Szín + + + Nyelv (Újraindítás) + + + Megosztási linkek importálása vágólapról (Ctrl+V) + + + QR kód beolvasása a képernyőről (Ctrl+S) + + + Kijelölt konfiguráció klónozása + + + Ismétlődő konfigurációk eltávolítása + + + Kijelölt konfigurációk eltávolítása (Delete) + + + Beállítás aktív konfigurációként (Enter) + + + Összes szolgáltatás statisztika törlése + + + Konfigurációk valós késleltetésének tesztelése (Ctrl+R) + + + Rendezés teszteredmény szerint + + + Konfigurációk letöltési sebességének tesztelése (Ctrl+T) + + + Konfigurációk tesztelése tcpinggel (Ctrl+O) + + + Kijelölt konfiguráció exportálása teljes konfigurációként + + + Megosztási link exportálása vágólapra (Ctrl+C) + + + Egyéni konfiguráció hozzáadása + + + [Shadowsocks] konfiguráció hozzáadása + + + [SOCKS] konfiguráció hozzáadása + + + [Trojan] konfiguráció hozzáadása + + + [VLESS] konfiguráció hozzáadása + + + [VMess] konfiguráció hozzáadása + + + Összes kijelölése (Ctrl+A) + + + Összes törlése + + + Másolás (Ctrl+C) + + + Összes másolása + + + Összes kijelölése (Ctrl+A) + + + Hozzáadás + + + Törlés + + + Szerkesztés + + + Megosztás + + + Frissítés engedélyezése + + + Rendezés + + + User Agent + + + Mégsem + + + Megerősítés + + + Szállítás + + + Cím + + + Nem biztonságos engedélyezése + + + ALPN + + + Alter ID + + + Ujjlenyomat + + + Álcázási típus + + + UUID(id) + + + Szállítási protokoll(hálózat) + + + Elérési út + + + Port + + + Alias (megjegyzések) + + + Álcázási tartomány(host) + + + Titkosítási módszer (biztonság) + + + SNI + + + TLS + + + *Alapértelmezett érték tcp + + + Core Típus + + + Flow + + + Generálás + + + Jelszó + + + Jelszó(Opcionális) + + + UUID(id) + + + Titkosítás + + + Felhasználó(Opcionális) + + + Titkosítás + + + Socks port + + + * A beállítás után egy socks szolgáltatás indul az Xray/sing-box(Tun) segítségével, hogy olyan funkciókat biztosítson, mint a sebességkijelzés + + + Tallózás + + + Szerkesztés + + + Haladó proxy beállítások, protokoll kiválasztása (opcionális) + + + Kapcsolatok engedélyezése a LAN-ról + + + Automatikus elrejtés indításkor + + + Geo fájlok automatikus frissítési intervalluma (órák) + + + Core: alapbeállítások + + + V2ray DNS beállítások + + + Core: KCP beállítások + + + Core Típus beállítások + + + Nem biztonságos engedélyezése + + + Kimenő Freedom tartomány stratégia + + + Oszlopszélesség automatikus beállítása előfizetés frissítése után + + + Előzetes kiadás frissítések ellenőrzése + + + Kivétel + + + Kivételek: Ne használjon proxy szervert a következő címmel kezdődő címekhez. Pontosvesszővel (;) válassza el a bejegyzéseket. + + + Valós idejű sebesség megjelenítése (újraindítást igényel) + + + Régebbi bejegyzések megtartása deduplikáláskor + + + Naplózás engedélyezése + + + Naplózási szint + + + Mux Multiplexing bekapcsolása + + + v2rayN beállítások + + + Hitelesítési jelszó + + + Egyéni DNS (több, vesszővel (,) elválasztva) + + + Win10 UWP Loopback beállítása + + + Sniffing bekapcsolása + + + Vegyes port + + + Indítás rendszerindításkor + + + Forgalmi statisztikák engedélyezése (újraindítást igényel) + + + Előfizetés konverziós URL + + + Rendszerproxy beállítások + + + Biztonsági protokoll TLS v1.3 engedélyezése (előfizetés/frissítés) + + + Tálca jobb egérgombos menü konfigurációk megjelenítési limitje + + + UDP engedélyezése + + + Hitelesítési felhasználó + + + Rendszerproxy törlése + + + GUI megjelenítése + + + Globális gyorsbillentyű beállítások + + + Közvetlenül beállítható billentyűnyomással; újraindítás után lép életbe + + + Ne változtassa meg a rendszerproxyt + + + Visszaállítás + + + Rendszerproxy beállítása + + + PAC mód + + + Konfiguráció megosztása (Ctrl+F) + + + Útválasztás + + + Nem rendszergazdaként fut + + + Futtatás rendszergazdaként + + + Mozgatás alulra (B) + + + Le (D) + + + Mozgatás felülre (T) + + + Fel (U) + + + Szűrő, támogatja a reguláris kifejezéseket + + + {0} Weboldal + + + Hozzáadás + + + Szabályok importálása + + + Kijelölt eltávolítása (Delete) + + + Beállítás aktív szabályként (Enter) + + + Tartomány stratégia + + + Előre definiált szabálykészlet lista + + + *Szabályok elválasztása vesszővel (,); Szó szerinti vesszőhöz használja a <COMMA>-t; Előtag # a szabály figyelmen kívül hagyásához + + + Szabályok importálása vágólapról + + + Szabályok importálása fájlból + + + Szabályok importálása előfizetési URL-ből + + + Szabály beállítások + + + Szabály hozzáadása + + + Kijelölt szabályok exportálása + + + Szabálylista + + + Szabály eltávolítása (Delete) + + + Útválasztási szabály részleteinek beállítása + + + Tartomány, IP, folyamat automatikusan rendeződik mentéskor + + + Szabály objektum dokumentum + + + Támogatja a DNS objektumot; Kattintson a dokumentáció megtekintéséhez + + + Csoport esetén hagyja üresen + + + Útválasztási beállítás megváltozott + + + Rendszerproxy beállítás megváltozott + + + Csak útválasztás + + + Ne használjon proxy szervert helyi (intranet) címekhez + + + Egykattintásos többszörös késleltetés és sebesség teszt (Ctrl+E) + + + Késleltetés (ms) + + + Sebesség (M/s) + + + Nem sikerült futtatni a Core-t, kérjük, ellenőrizze a prompt információt + + + Megjegyzések reguláris szűrője + + + Napló megjelenítése + + + Tun engedélyezése + + + Új port a LAN-hoz + + + Tun mód beállítások + + + Mozgatás csoportba + + + Konfigurációk rendezésének engedélyezése húzással (újraindítást igényel) + + + Automatikus frissítés + + + Teszt kihagyása + + + Konfiguráció szerkesztése (Ctrl+D) + + + Dupla kattintás a konfigurációra aktiválja + + + Teszt befejeződött + + + Alapértelmezett TLS ujjlenyomat + + + User-Agent + + + Ez a paraméter csak tcp/http és ws esetén érvényes + + + Betűtípus (újraindítást igényel) + + + Másolja a TTF/TTC betűtípus fájlt a gui Fonts könyvtárba; Nyissa meg újra a beállítások ablakot + + + Pac port = +3; Xray API port = +4; mihomo API port = +5; + + + Rendszergazdai jogosultságokkal állítsa be, indítás után szerezzen rendszergazdai jogosultságokat + + + Betűméret + + + Sebességteszt egyszeri időtúllépési érték + + + Sebességteszt URL + + + Mozgatás fel és le + + + Nyilvános kulcs + + + Rövid azonosító + + + Spider X + + + Hardveres gyorsítás engedélyezése (újraindítást igényel) + + + Tesztelésre vár (ESC megnyomásával megszakítható)... + + + Kérjük, kapcsolja ki rendellenes megszakadás esetén + + + A frissítések nincsenek engedélyezve, kihagyja ezt az előfizetést + + + Újraindítás rendszergazdaként + + + További URL-ek, vesszővel elválasztva; Az előfizetés konverzió érvénytelen lesz + + + {0} : {1}/s↑ | {2}/s↓ + + + Automatikus frissítési intervallum (percek) + + + Naplózás engedélyezése fájlba + + + Konverziós cél típus + + + Kérjük, hagyja üresen, ha nincs szükség konverzióra + + + DNS beállítások + + + sing-box DNS beállítások + + + Kérjük, töltse ki a DNS struktúrát, kattintson a dokumentum megtekintéséhez + + + Kattintson az alapértelmezett DNS konfiguráció importálásához + + + sing-box tartomány stratégia + + + sing-box Mux protokoll + + + Teljes folyamatnév (Tun mód) + + + IP vagy IP CIDR + + + Tartomány + + + Hysteria2 konfiguráció hozzáadása + + + Hysteria Max sávszélesség (Fel/Le) + + + Rendszer Hosts használata + + + TUIC konfiguráció hozzáadása + + + Torlódásvezérlés + + + Előző proxy konfiguráció megjegyzései + + + Következő proxy konfiguráció megjegyzései + + + Kérjük, győződjön meg arról, hogy a konfigurációs megjegyzések léteznek és egyediek + + + Automatikus útválasztás + + + Szigorú útválasztás + + + Hálózati verem + + + MTU + + + További bejövő engedélyezése + + + IPv6 cím engedélyezése + + + WireGuard konfiguráció hozzáadása + + + Privát kulcs + + + Fenntartott (2,3,4) + + + Cím (IPv4, IPv6) + + + obfs jelszó + + + (Tartomány vagy IP vagy folyamatnév) és port és protokoll és bejövő címke => kimenő címke + + + Automatikus görgetés a végére + + + Sebesség Ping Teszt URL + + + Előfizetés frissítése, csak a megjegyzések létezésének ellenőrzése + + + Teszt megszakítása... + + + *grpc Authority + + + HTTP konfiguráció hozzáadása + + + which conflicts with the group previous proxy + + + Fragment engedélyezése + + + Gyorsítótár fájl engedélyezése sing-boxhoz (szabálykészlet fájlok) + + + A sing-box szabálykészletének testreszabása + + + Sikeres művelet. Kattintson a beállítások menüre az alkalmazás újraindításához. + + + Fájl helyének megnyitása + + + Rendezés + + + Lánc + + + Alapértelmezett + + + Késleltetés + + + Letöltési sebesség + + + Letöltési forgalom + + + Host + + + Név + + + Hálózat + + + Idő + + + Típus + + + Feltöltési sebesség + + + Feltöltési forgalom + + + Kapcsolatok + + + Kapcsolat bezárása + + + Összes kapcsolat bezárása + + + Proxyk + + + Szabály mód + + + Közvetlen + + + Globális + + + Ne változtassa meg + + + Szabály + + + Késleltetés teszt + + + Részleges csomópont késleltetés teszt + + + Proxyk frissítése + + + Aktív csomópont kiválasztása (Enter) + + + Alapértelmezett tartomány stratégia kimenő forgalomhoz + + + Fő elrendezés iránya (újraindítást igényel) + + + Kimenő DNS cím + + + Oszlopszélesség automatikus beállítása + + + Base64-kódolt megosztási linkek exportálása vágólapra + + + Kijelölt konfiguráció exportálása teljes konfigurációként a vágólapra + + + Főablak megjelenítése vagy elrejtése + + + Egyéni konfiguráció socks portja + + + Biztonsági mentés és visszaállítás + + + Biztonsági mentés helyi tárolóba + + + Visszaállítás helyi tárolóból + + + Biztonsági mentés távoli helyre (WebDAV) + + + Visszaállítás távoli helyről (WebDAV) + + + Helyi + + + Távoli (WebDAV) + + + WebDAV URL + + + WebDAV felhasználónév + + + WebDAV jelszó + + + WebDAV ellenőrzés + + + Távoli mappa neve (opcionális) + + + Érvénytelen biztonsági mentés fájl + + + Host szűrő + + + Aktív + + + Geo fájlok forrása (opcionális) + + + sing-box szabálykészlet fájlok forrása (opcionális) + + + Frissítő alkalmazás nem létezik + + + Útválasztási szabályok forrása (opcionális) + + + Regionális előbeállítások + + + Alapértelmezett + + + Oroszország + + + Irán + + + Kínai régióban lévő felhasználók figyelmen kívül hagyhatják ezt az elemet + + + QR kód beolvasása a képből + + + Érvénytelen cím (URL) + + + Kérjük, ne használjon nem biztonságos HTTP protokoll előfizetési címet + + + Telepítse a betűtípust a rendszerbe, válassza ki vagy töltse ki a betűtípus nevét, indítsa újra a beállításokat + + + Biztosan ki akar lépni? + + + Megjegyzések + + + Rendszer sudo jelszó + + + A jelszót a parancssoron keresztül ellenőrizzük. Ha egy érvényesítési hiba miatt az alkalmazás hibásan működik, indítsa újra az alkalmazást. A jelszó nem kerül tárolásra, és minden újraindítás után újra meg kell adni. + + + *xhttp mód + + + XHTTP Extra nyers JSON, formátum: { XHTTP Objektum } + + + Ablak bezárásakor a tálcára rejtés + + + A párhuzamos tesztek száma több teszt során + + + Kivételek: Ne használjon proxy szervert a következő címekhez. Vesszővel (,) válassza el a bejegyzéseket. + + + Sniffing típus + + + Második vegyes port engedélyezése + + + socks: helyi port, socks2: második helyi port, socks3: LAN port + + + Téma + + + Proxy parancs másolása vágólapra + + + Sikertelen részek újratesztelése elindult, {0} maradt. ESC megnyomásával megszakítható... + + + Teszt eredmény szerint + + + Érvénytelenek eltávolítása teszteredmények alapján + + + Eltávolítva {0} érvénytelen teszteredmény. + + + Konfigurációs port tartomány + + + A portot lefedi, vesszővel (,) elválasztva + + + Több konfiguráció egyéni konfigurációra + + + Több konfiguráció véletlenszerűen Xray szerint + + + Több konfiguráció RoundRobin Xray szerint + + + Több konfiguráció legkisebb pinggel Xray szerint + + + Több konfiguráció legkisebb terheléssel Xray szerint + + + Több konfiguráció legkisebb pinggel sing-box szerint + + + Konfiguráció exportálása + + + Aktuális kapcsolat info teszt URL + + + Kitöltheti a konfigurációs megjegyzéseket, kérjük, győződjön meg róla, hogy létezik és egyedi + + + Helytelen jelszó, próbálja újra. + + + Mldsa65Verify + + + [Anytls] konfiguráció hozzáadása + + + Remote DNS + + + Domestic DNS + + + Outbound DNS Resolution (sing-box) + + + Resolve Outbound Domains + + + sing-box DoH Resolver Server + + + Fallback DNS Resolution, Suggest IP + + + xray Freedom Resolution Strategy + + + sing-box Direct Resolution Strategy + + + sing-box Remote Resolution Strategy + + + Add Common DNS Hosts + + + The sing-box DoH resolution server can be overwritten + + + FakeIP + + + Block SVCB and HTTPS Queries + + + DNS Hosts: ("domain1 ip1 ip2" per line) + + + Apply to Proxy Domains Only + + + Basic DNS Settings + + + Advanced DNS Settings + + + Validate Regional Domain IPs + + + When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs + + + Enable Custom DNS + + + Custom DNS Enabled, This Page's Settings Invalid + + + Prevent domain-based routing rules from failing + + + Please fill in the correct config template + + + Full Config Template Setting + + + Enable Full Config Template + + + v2ray Full Config Template + + + Add Outbound Config Only, routing.balancers and routing.rules.outboundTag, Click to view the document + + + Do Not Add Non-Proxy Protocol Outbound + + + Set Upstream Proxy Tag + + + sing-box Full Config Template + + + Add Outbound and Endpoint Config Only, Click to view the document + + + This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you. + + + Start parsing and processing subscription content + + + Select Profile + + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx new file mode 100644 index 00000000..614c8092 --- /dev/null +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -0,0 +1,1518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Export share link to clipboard successfully + + + Please check the Configuration settings first. + + + Invalid configuration format. + + + Note that custom configuration relies entirely on your own configuration and does not work with all settings. If you want to use the system proxy, please modify the listening port manually. + + + Downloading... + + + Failed to convert configuration file + + + Failed to generate default configuration file + + + Failed to get the default configuration + + + Failed to import custom configuration Configuration + + + Failed to read configuration file + + + Please enter the correct port format. + + + Please enter the local listening port. + + + Please enter the password. + + + Please enter the address. + + + Please enter the user ID. + + + This is not the correct configuration, please check + + + Initial Configuration + + + {0} {1} already up to date. + + + {0} {1} already up to date. + + + Address + + + Security + + + Port + + + Type + + + Subs group + + + Download traffic today + + + Upload traffic today + + + Total download traffic + + + Total upload traffic + + + Transport + + + Downloaded Core successfully + + + Failed to import subscription content + + + Got subscription content successfully + + + No valid subscriptions set + + + Resolved {0} successfully + + + Started getting subscriptions + + + Started updating {0}... + + + Invalid subscription content + + + Unpacking... + + + Subscription update ended + + + Subscription update started + + + Updated Core successfully + + + Updated Core successfully! Restarting service... + + + Non-VMess or SS protocol + + + The Core file (file name: {1}) was not found under the folder ({0}), please download and put it in the folder, download address: {2} + + + Scan completed, no valid QR code found + + + Operation failed, please check and retry + + + Please fill Remarks + + + Please select the encryption method + + + Please select a protocol + + + Please select the Configuration first + + + Configurations deduplication completed. Old: {0}, New: {1}. + + + Are you sure you want to remove the Configuration? + + + The client configuration file is saved at: {0} + + + Starting service ({0})... + + + Configuration successful. {0} + + + Custom configuration Configuration imported successfully + + + {0} Configurations have been imported from clipboard + + + Successfully scanned and imported the shared link + + + The delay: {0} ms, {1} + + + Operation successful + + + Please select rules + + + Are you sure you want to remove the rules? + + + {0}, one of the required fields. + + + Remarks + + + URL (optional) + + + Count + + + Please enter the URL + + + Do you want to append rules? Choose yes to append, no to replace. + + + Downloaded GeoFile: {0} successfully + + + Information + + + Custom icon + + + Please fill in the correct custom DNS + + + *ws/http upgrade/xhttp path + + + *h2 path + + + *QUIC key/KCP seed + + + *grpc service name + + + *http host separated by commas (,) + + + *ws/http upgrade/xhttp host + + + *h2 host separated by commas (,) + + + *QUIC security + + + *tcp camouflage type + + + *kcp camouflage type + + + *QUIC camouflage type + + + *grpc mode + + + TLS + + + *kcp seed + + + Global hotkey {0} registration failed, reason: {1} + + + Global hotkey {0} registered successfully + + + All + + + Please browse to import Configuration configuration + + + Testing... + + + LAN + + + Local + + + Configuration filter, press Enter to execute + + + Check Update + + + Close + + + Exit + + + Global Hotkey Setting + + + Help + + + Option Setting + + + Promotion + + + Reload + + + Routing Setting + + + Configurations + + + Settings + + + Update current subscription without proxy + + + Update current subscription with proxy + + + Subscription Group + + + Subscription group settings + + + Update subscriptions without proxy + + + Update subscriptions with proxy + + + System proxy + + + Clear system proxy + + + Do not change system proxy + + + PAC mode + + + Set system proxy + + + Color + + + Language (Restart) + + + Import Share Links from clipboard (Ctrl+V) + + + Scan QR code on the screen (Ctrl+S) + + + Clone selected Configuration + + + Remove duplicate Configurations + + + Remove selected Configurations (Delete) + + + Set as active Configuration (Enter) + + + Clear all service statistics + + + Test Configurations real delay (Ctrl+R) + + + Sort by test result + + + Test Configurations download speed (Ctrl+T) + + + Test Configurations with tcping (Ctrl+O) + + + Export selected Configuration for complete configuration + + + Export Share Link to Clipboard (Ctrl+C) + + + Add a custom configuration Configuration + + + Add [Shadowsocks] Configuration + + + Add [SOCKS] Configuration + + + Add [Trojan] Configuration + + + Add [VLESS] Configuration + + + Add [VMess] Configuration + + + Select all (Ctrl+A) + + + Clear all + + + Copy (Ctrl+C) + + + Copy all + + + Select all (Ctrl+A) + + + Add + + + Delete + + + Edit + + + Share + + + Enable update + + + Sort + + + User Agent + + + Cancel + + + Confirm + + + Transport + + + Address + + + Allow Insecure + + + ALPN + + + Alter ID + + + Fingerprint + + + Camouflage type + + + UUID(id) + + + Transport protocol(network) + + + Path + + + Port + + + Alias (remarks) + + + Camouflage domain(host) + + + Encryption method (security) + + + SNI + + + TLS + + + *Default value tcp + + + Core Type + + + Flow + + + Generate + + + Password + + + Password(Optional) + + + UUID(id) + + + Encryption + + + User(Optional) + + + Encryption + + + Socks port + + + * After setting this value, a socks service will be started using Xray/sing-box(Tun) to provide functions such as speed display + + + Browse + + + Edit + + + Advanced proxy settings, protocol selection (optional) + + + Allow connections from the LAN + + + Auto hide on startup + + + Automatic update interval for Geo files (hours) + + + Core: basic settings + + + V2ray DNS settings + + + Core: KCP settings + + + Core Type settings + + + Allow Insecure + + + Outbound Freedom domain Strategy + + + Automatically adjust column width after subscription update + + + Check for pre-release updates + + + Exception + + + Exclusions: Do not use proxy server for addresses beginning with the following. Use semicolon (;) to separate entries. + + + Display real-time speed (requires restart) + + + Keep older entries when de-duplicating + + + Enable Log + + + Log Level + + + Turn on Mux Multiplexing + + + v2rayN settings + + + Auth pass + + + Custom DNS (multiple, separated by commas (,)) + + + Set Win10 UWP Loopback + + + Turn on Sniffing + + + Mixed Port + + + Start on boot + + + Enable traffic statistics (requires restart) + + + Subscription conversion URL + + + System proxy settings + + + Enable Security Protocol TLS v1.3 (subscription/update) + + + Tray right-click menu Configurations display limit + + + Enable UDP + + + Auth user + + + Clear system proxy + + + Display GUI + + + Global Hotkey Settings + + + Set directly by pressing the keyboard; takes effect after restart + + + Do not change system proxy + + + Reset + + + Set system proxy + + + PAC mode + + + Share Configuration (Ctrl+F) + + + Routing + + + Not run as Admin + + + Run as Admin + + + Move to bottom (B) + + + Down (D) + + + Move to top (T) + + + Up (U) + + + Filter, supports regular expressions + + + {0} Website + + + Add + + + Import Rules + + + Remove selected (Delete) + + + Set as active rule (Enter) + + + Domain strategy + + + Pre-defined Rule Set List + + + *Separate rules by commas (,); For a literal comma use <COMMA>; Prefix # to ignore a rule + + + Import Rules From Clipboard + + + Import Rules From File + + + Import Rules From Subscription URL + + + Rule Settings + + + Add Rule + + + Export Selected Rules + + + Rule List + + + Remove Rule (Delete) + + + Routing Rule Details Setting + + + Domain, IP, process are auto-sorted when saving + + + Rule object Doc + + + Supports DNS Object; Click to view documentation + + + For group please leave blank here + + + Routing setting has changed + + + System proxy setting has changed + + + Route Only + + + Do not use proxy servers for local (intranet) addresses + + + One-click multi-test latency and speed (Ctrl+E) + + + Delay (ms) + + + Speed (M/s) + + + Failed to run Core, please check the prompt information + + + Remarks regular filter + + + Display Log + + + Enable Tun + + + New Port for LAN + + + Tun Mode settings + + + Move to group + + + Enable sorting Configurations by drag-n-drop (requires restart) + + + Auto refresh + + + Skip test + + + Edit Configuration (Ctrl+D) + + + Double-clicking Configuration makes it active + + + Test completed + + + Default TLS fingerprint + + + User-Agent + + + This parameter is valid only for tcp/http and ws + + + Font family (requires restart) + + + Copy the font TTF/TTC file to the directory gui Fonts; Reopen the settings window + + + Pac port = +3; Xray API port = +4; mihomo API port = +5; + + + Set this with admin privileges, get admin privileges after startup + + + Font Size + + + Speed Test Single Timeout Value + + + Speed Test URL + + + Move up and down + + + Public Key + + + Short Id + + + Spider X + + + Enable hardware acceleration (requires restart) + + + Waiting for testing (press ESC to terminate)... + + + Please turn off when there is an abnormal disconnection + + + Updates are not enabled, skip this subscription + + + Restart as Administrator + + + More URLs, separated by commas; Subscription conversion will be invalid + + + {0} : {1}/s↑ | {2}/s↓ + + + Automatic update interval (minutes) + + + Enable logging to file + + + Convert target type + + + Please leave blank if no conversion is required + + + DNS Settings + + + sing-box DNS settings + + + Please fill in DNS Structure, Click to view the document + + + Click to import default DNS config + + + sing-box domain strategy + + + sing-box Mux Protocol + + + Full process name (Tun mode) + + + IP or IP CIDR + + + Domain + + + Add [Hysteria2] Configuration + + + Hysteria Max bandwidth (Up/Down) + + + Use System Hosts + + + Add [TUIC] Configuration + + + Congestion control + + + Previous proxy Configuration remarks + + + Next proxy Configuration remarks + + + Please make sure the Configuration remarks exist and are unique + + + Auto Route + + + Strict Route + + + Stack + + + MTU + + + Enable additional Inbound + + + Enable IPv6 Address + + + Add [WireGuard] Configuration + + + Private Key + + + Reserved (2,3,4) + + + Address (IPv4, IPv6) + + + obfs password + + + (Domain or IP or Proc Name) and Port and Protocol and Inbound Tag => Outbound Tag + + + Auto scroll to end + + + Speed Ping Test URL + + + Updating subscription, only determining if remarks exist + + + Test terminating... + + + *grpc Authority + + + Add [HTTP] Configuration + + + which conflicts with the group previous proxy + + + Enable fragment + + + Enable cache file for sing-box (ruleset files) + + + Customize the rule-set of sing-box + + + Successful operation. Click the settings menu to reboot the app. + + + Open the storage location + + + Sorting + + + Chain + + + Default + + + Delay + + + Download Speed + + + Download Traffic + + + Host + + + Name + + + Network + + + Time + + + Type + + + Upload Speed + + + Upload Traffic + + + Connections + + + Close Connection + + + Close All Connections + + + Proxies + + + Rule mode + + + Direct + + + Global + + + Do not change + + + Rule + + + Latency Test + + + Part Node Latency Test + + + Refresh Proxies + + + Select active node (Enter) + + + Default domain strategy for outbound + + + Main layout orientation (requires restart) + + + Outbound DNS address + + + Auto column width adjustment + + + Export Base64-encoded Share Links to Clipboard + + + Export selected Configuration for complete configuration to clipboard + + + Show or hide the main window + + + Custom config socks port + + + Backup and Restore + + + Backup to local + + + Restore from local + + + Backup to remote (WebDAV) + + + Restore from remote (WebDAV) + + + Local + + + Remote (WebDAV) + + + WebDAV URL + + + WebDAV User Name + + + WebDAV Password + + + WebDAV Check + + + Remote folder name (optional) + + + Invalid backup file + + + Host filter + + + Active + + + Geo files source (optional) + + + sing-box ruleset files source (optional) + + + Upgrade App does not exist + + + Routing rules source (optional) + + + Regional presets setting + + + Default + + + Russia + + + Iran + + + Users in China region can ignore this item + + + Scan QR code in the image + + + Invalid address (URL) + + + Please do not use the insecure HTTP protocol subscription address + + + Install the font to the system, select or fill in the font name, restart the settings + + + Are you sure you want to exit? + + + Remarks Memo + + + System sudo password + + + The password will be validated via the command line. If a validation error causes the application to malfunction, please restart the application. The password will not be stored and must be entered again after each restart. + + + *xhttp mode + + + XHTTP Extra raw JSON, format: { XHTTP Object } + + + Hide to tray when closing the window + + + The number of concurrent tests during multi-test + + + Exclusions: Do not use proxy server for the following addresses. Use comma (,) to separate entries. + + + Sniffing type + + + Enable second mixed port + + + socks: local port, socks2: second local port, socks3: LAN port + + + Theme + + + Copy proxy command to clipboard + + + Starting retesting failed parts, {0} remaining. Press ESC to terminate... + + + By test result + + + Remove invalid by test results + + + Removed {0} invalid test results. + + + Configuration port range + + + Will cover the port, separate with commas (,) + + + Multi-Configuration to custom configuration + + + Multi-Configuration Random by Xray + + + Multi-Configuration RoundRobin by Xray + + + Multi-Configuration LeastPing by Xray + + + Multi-Configuration LeastLoad by Xray + + + Multi-Configuration LeastPing by sing-box + + + Export Configuration + + + Current connection info test URL + + + Can fill in the configuration remarks, please make sure it exist and are unique + + + Incorrect password, please try again. + + + Mldsa65Verify + + + Add [Anytls] Configuration + + + Remote DNS + + + Domestic DNS + + + Outbound DNS Resolution (sing-box) + + + Resolve Outbound Domains + + + sing-box DoH Resolver Server + + + Fallback DNS Resolution, Suggest IP + + + xray Freedom Resolution Strategy + + + sing-box Direct Resolution Strategy + + + sing-box Remote Resolution Strategy + + + Add Common DNS Hosts + + + The sing-box DoH resolution server can be overwritten + + + FakeIP + + + Block SVCB and HTTPS Queries + + + DNS Hosts: ("domain1 ip1 ip2" per line) + + + Apply to Proxy Domains Only + + + Basic DNS Settings + + + Advanced DNS Settings + + + Validate Regional Domain IPs + + + When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs + + + Enable Custom DNS + + + Custom DNS Enabled, This Page's Settings Invalid + + + Prevent domain-based routing rules from failing + + + Please fill in the correct config template + + + Full Config Template Setting + + + Enable Full Config Template + + + v2ray Full Config Template + + + Add Outbound Config Only, routing.balancers and routing.rules.outboundTag, Click to view the document + + + Do Not Add Non-Proxy Protocol Outbound + + + Set Upstream Proxy Tag + + + sing-box Full Config Template + + + Add Outbound and Endpoint Config Only, Click to view the document + + + This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you. + + + Start parsing and processing subscription content + + + Select Profile + + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx new file mode 100644 index 00000000..294d9f34 --- /dev/null +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -0,0 +1,1518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Ссылка успешно скопирована в буфер обмена + + + Сначала проверьте настройки сервера + + + Недопустимый формат конфигурации + + + Обратите внимание, что пользовательская конфигурация полностью зависит от вашей собственной конфигурации и работает не со всеми настройками. Если вы хотите использовать системный прокси, то измените порт прослушивания вручную + + + Загрузка... + + + Не удалось преобразовать файл конфигурации + + + Не удалось создать файл конфигурации по умолчанию + + + Не удалось получить конфигурацию по умолчанию + + + Не удалось импортировать сервер пользовательской конфигурации + + + Не удалось прочитать файл конфигурации + + + Укажите порт сервера в правильном формате + + + Введите локальный порт для прослушивания + + + Введите пароль + + + Введите адрес сервера + + + Введите идентификатор пользователя + + + Некорректная конфигурация. Проверьте + + + Исходная конфигурация + + + {0} {1} является последней версией + + + {0} {1} является последней версией + + + Адрес + + + Метод шифрования + + + Порт + + + Тип + + + Группа подписки + + + Загружено трафика сегодня + + + Отправлено сегодня + + + Всего загружено + + + Всего отдано + + + Протокол + + + Ядро успешно загружено + + + Не удалось импортировать подписку + + + Содержимое подписки успешно импортировано + + + Нет установленных подписок + + + Парсинг {0} прошел успешно + + + Начинаю получать подписки + + + Начинаю обновление {0}... + + + Некорректное содержимое подписки + + + Распаковка… + + + Обновление подписки закончено + + + Обновление подписки начинается + + + Ядро успешно обновлено + + + Успешное обновление ядра! Перезапуск службы... + + + Не является протоколом VMess или Shadowsocks + + + Файл ядра ({1}) не найден в папке {0}. Скачайте по адресу {2} и поместите его туда + + + Сканирование завершено, не найден корректный QR код + + + Операция безуспешна, проверьте и попробуйте ещё раз + + + Введите примечания + + + Выберите метод шифрования + + + Выберите протокол + + + Сначала выберите сервер + + + Удаление дублей завершено. Старая: {0}, Новая: {1} + + + Вы уверены, что хотите удалить сервер? + + + Файл конфигурации клиента сохранен по адресу: {0} + + + Запуск сервиса ({0})... + + + Конфигурация выполнена успешно {0} + + + Пользовательская конфигурация сервера успешно импортирована + + + {0} серверов импортировано из буфера обмена + + + Сканирование URL-адреса импорта прошло успешно + + + Задержка текущего сервера: {0} мс, {1} + + + Операция успешна + + + Выберите правила + + + Вы уверены, что хотите удалить правила? + + + {0}: одно из обязательных полей + + + Примечания + + + URL (необязательно) + + + Количество + + + Введите URL-адрес + + + Хотите добавить правила? Выберите «Да» для добавления или «Нет» для замены + + + Загрузка GeoFile {0} прошла успешно + + + Информация + + + Пользовательская иконка + + + Введите корректный пользовательский DNS + + + *WebSocket-путь + + + *HTTP2-путь + + + *QUIC-ключ / KCP-seed + + + Имя сервиса *gRPC + + + *http-хосты, разделённые запятыми (,) + + + *WebSocket-хост + + + *HTTP2-хосты, разделённые запятыми (,) + + + Безопасность *QUIC + + + Тип *TCP-камуфляжа + + + Тип *KCP-камуфляжа + + + Тип *QUIC-камуфляжа + + + Режим *gRPC + + + TLS + + + *KCP-seed + + + Не удалось зарегистрировать глобальную горячую клавишу {0}, причина: {1} + + + Глобальная горячая клавиша {0} зарегистрирована успешно + + + Все серверы + + + Выберите файл конфигурации сервера для импорта + + + Тестирование... + + + LAN + + + Локальный + + + Фильтр серверов + + + Проверить обновления + + + Закрыть + + + Выход + + + Глобальная настройка горячих клавиш + + + Помощь + + + Настройка параметров + + + Содействие + + + Перезагрузка + + + Настройки маршрутизации + + + Серверы + + + Настройки + + + Обновить текущую подписку без прокси + + + Обновить подписку через прокси + + + Группа подписки + + + Настройки группы подписки + + + Обновить подписку без прокси + + + Обновить подписку с прокси + + + Системный прокси + + + Очистить системный прокси + + + Не менять системный прокси + + + Режим PAC + + + Установить системный прокси + + + Цвет + + + Язык (требуется перезапуск) + + + Импорт массива URL из буфера обмена (Ctrl+V) + + + Сканировать QR-код с экрана (Ctrl+S) + + + Клонировать выбранный сервер + + + Удалить дубликаты серверов + + + Удалить выбранные серверы (Delete) + + + Установить как активный сервер (Enter) + + + Очистить всю статистику + + + Тест на реальную задержку сервера (Ctrl+R) + + + Сортировать по результату теста + + + Тест на скорость загрузки сервера (Ctrl+T) + + + Тест задержки с tcping (Ctrl+O) + + + Экспортировать выбранный сервер для клиента + + + Экспорт URL-адресов общего доступа в буфер обмена (Ctrl+C) + + + Добавить сервер пользовательской конфигурации + + + Добавить сервер [Shadowsocks] + + + Добавить сервер [SOCKS] + + + Добавить сервер [Trojan] + + + Добавить сервер [VLESS] + + + Добавить сервер [VMess] + + + Выбрать все (Ctrl+A) + + + Очистить все + + + Скопировать (Ctrl+C) + + + Скопировать все + + + Выбрать все (Ctrl+A) + + + Добавить + + + Удалить + + + Редактировать + + + Поделиться + + + Включены обновления + + + Сортировка + + + Заголовок User-Agent + + + Отмена + + + Подтвердить + + + Транспорт + + + Адрес + + + Разрешить небезопасные + + + ALPN + + + AlterId + + + Отпечаток + + + Тип камуфляжа + + + UUID (id) + + + Транспортный протокол сети + + + Путь + + + Порт + + + Примечание + + + Маскирующий домен (хост) + + + Метод шифрования + + + SNI + + + TLS + + + *По-умолчанию TCP + + + Ядро + + + Поток + + + Генерировать + + + Пароль + + + Пароль(Необязательно) + + + UUID(id) + + + Шифрование + + + Пользователь(Необязательно) + + + Шифрования + + + Порт SOCKS + + + * После установки этого значения служба SOCKS будет запущена с использованием Xray/sing-box(TUN) для обеспечения таких функций, как отображение скорости + + + Просмотр + + + Редактировать + + + Расширенные настройки прокси, выбор протокола (необязательно) + + + Разрешить подключения из локальной сети + + + Автоскрытие при автозапуске + + + Интервал автоматического обновления Geo в часах + + + Ядро: базовые настройки + + + Настройки DNS V2ray + + + Ядро: настройки KCP + + + Настройки типа ядра + + + Разрешить небезопасные + + + «Freedom»: стратегия обработки доменов исходящего трафика + + + Автоматически регулировать ширину столбца после обновления подписки + + + Проверить наличие предварительных обновлений + + + Исключение + + + Исключение. Не используйте прокси-сервер для адресов, начинающихся с (,), используйте точку с запятой (;) + + + Показывать скорость в реальном времени + + + Сохранить старые при удалении дублей + + + Записывать логи + + + Уровень записи логов + + + Включить мультиплексирование Mux + + + Настройки v2rayN + + + Пароль аутентификации + + + Пользовательский DNS (если несколько, то делите запятыми (,)) + + + Разрешить loopback для приложений UWP (Win10) + + + Включить сниффинг + + + Смешанный порт + + + Автозапуск + + + Включить статистику (требуется перезагрузка) + + + URL конвертации подписок + + + Настройки системного прокси + + + Включить протокол безопасности TLS v1.3 (обновление подписки) + + + Лимит серверов в меню трея + + + Включить UDP + + + Имя пользователя (логин) + + + Очистить системный прокси + + + Показать GUI + + + Настройка горячих клавиш + + + Установите непосредственно, нажав на клавиатуру, вступит в силу после перезапуска + + + Не изменять системный прокси + + + Обнулить + + + Установить системный прокси + + + Режим PAC + + + Поделиться сервером (Ctrl+F) + + + Маршрутизация + + + Пользователь + + + Администратор + + + Спуститься вниз (B) + + + Вниз (D) + + + Подняться наверх (T) + + + Вверх (U) + + + Фильтр, поддерживает regex + + + {0} веб-сайт + + + Добавить + + + Добавить расширенные правила + + + Удалить выбранные + + + Установить как активное правило + + + Доменная стратегия + + + Предустановленный список наборов правил + + + *Разделяйте правила запятыми (,). Литерал «,» — <COMMA>. Префикс # — отключить правило + + + Импорт правил из буфера обмена + + + Импорт правил из файла + + + Импорт правил из URL + + + Настройка правил + + + Добавить правило + + + Экспорт выделенных правил + + + Список правил + + + Удалить правила (Delete) + + + Детальные настройки правил маршрутизации + + + Домен и IP автоматически сортируются при сохранении + + + Документация RuleObject + + + Поддерживаются DNS-объекты, нажмите для просмотра документации + + + Необязательное поле + + + Настройки маршрутизации изменены + + + Системные прокси изменены + + + Только маршрут + + + Не используйте прокси-серверы для локальных (интранет) адресов + + + Тест задержки и скорости всех серверов (Ctrl+E) + + + Задержка (мс) + + + Скорость (МБ/с) + + + Не удалось запустить ядро, посмотрите логи + + + Фильтр примечаний (Regex) + + + Показать логи + + + Режим VPN + + + Новый порт для локальной сети + + + Настройки режима TUN + + + Перейти в группу + + + Включить сортировку перетаскиванием сервера (требуется перезагрузка) + + + Автообновление + + + Пропустить тест + + + Редактировать сервер (Ctrl+D) + + + Двойной клик чтобы сделать сервер активным + + + Тест завершен + + + TLS отпечаток по умолчанию + + + User-Agent + + + Параметр действует только для TCP/HTTP и WebSocket (WS) + + + Шрифт (требуется перезагрузка) + + + Скопируйте файл шрифта TTF/TTC в каталог guiFonts и заново откройте окно настроек + + + Pac порт = +3,Xray API порт = +4, mihomo API порт = +5 + + + Установите это с правами администратора + + + Размер шрифта + + + Тайм-аут одиночного тестирования скорости + + + URL для тестирования скорости + + + Переместить вверх/вниз + + + PublicKey + + + ShortId + + + SpiderX + + + Включить аппаратное ускорение (требуется перезагрузка) + + + Ожидание тестирования (нажмите ESC для отмены)… + + + Отключите при аномальном разрыве соединения + + + Обновления не включены — подписка пропущена + + + Перезагрузить как администратор + + + Дополнительные URL через запятую, конвертация подписки недоступна + + + {0} : {1}/s↑ | {2}/s↓ + + + Интервал автоматического обновления в минутах + + + Включить запись логов в файл + + + Преобразовать тип цели + + + Если преобразование не требуется, оставьте поле пустым + + + Настройки DNS + + + Настройки DNS sing-box + + + Заполните структуру DNS, нажмите, чтобы открыть документ + + + Нажмите, чтобы импортировать конфигурацию DNS по умолчанию + + + Стратегия домена для sing-box + + + Протокол Mux для sing-box + + + Полное имя процесса (режим TUN) + + + IP-адрес или сеть CIDR + + + Домен + + + Добавить сервер [Hysteria2] + + + Максимальная пропускная способность Hysteria (загрузка/отдача) + + + Использовать системные узлы + + + Добавить сервер [TUIC] + + + Контроль перегрузок + + + Примечания к предыдущему прокси + + + Примечания к следующему прокси + + + Убедитесь, что примечание существует и является уникальным + + + Автоматическая маршрутизация + + + Строгая маршрутизация + + + Сетевой стек + + + MTU + + + Включить дополнительный входящий канал + + + Включить IPv6 адреса + + + Добавить сервер [WireGuard] + + + Приватный ключ + + + Зарезервировано (2, 3, 4) + + + Адрес (Ipv4,Ipv6) + + + Пароль obfs + + + (Домен или IP или имя процесса) и порт, и протокол, и InboundTag => OutboundTag + + + Автоматическая прокрутка в конец + + + URL для быстрой проверки реальной задержки + + + Обновляя подписку, проверять лишь наличие примечаний + + + Отмена тестирования... + + + * gRPC Authority (HTTP/2 псевдозаголовок :authority) + + + Добавить сервер [HTTP] + + + что конфликтует с предыдущим прокси группы + + + Включить фрагментацию (Fragment) + + + Включить файл кэша для sing-box (файлы наборов правил) + + + Пользовательский набор правил для sing-box + + + Операция успешна. Перезапустите приложение + + + Открыть место хранения + + + Сортировка + + + Цепочка + + + По умолчанию + + + Задержка + + + Скорость загрузки + + + Скачанный трафик + + + Узел + + + Имя + + + Сеть + + + Время + + + Тип + + + Скорость отдачи + + + Отправленный трафик + + + Соединения + + + Закрыть соединение + + + Закрыть все соединения + + + Прокси + + + Режим правила + + + Прямое соединение + + + Глобальный режим + + + Не менять + + + Правила + + + Тест задержки + + + Тест задержки выбранных узлов + + + Обновить прокси + + + Сделать узел активным (Enter) + + + Стратегия домена по умолчанию для исходящих + + + Основная ориентация макета (требуется перезагрузка) + + + Исходящий DNS адрес + + + Автоматическая регулировка ширины столбца + + + Экспорт ссылок в формате Base64 в буфер обмена + + + Экспортировать выбранный сервер для полной конфигурации в буфер обмена + + + Показать или скрыть главное окно + + + Пользовательская конфигурация порта SOCKS + + + Резервное копирование и восстановление + + + Сохранить в файл + + + Восстановить из файла + + + Резервное копирование на удалённый сервер (WebDAV) + + + Восстановить с удалённого сервера (WebDAV) + + + Локально + + + Удалённо (WebDAV) + + + URL WebDAV + + + Имя пользователя WebDAV + + + Пароль WebDAV + + + Проверить WebDAV + + + Имя удаленной папки (необязательно) + + + Неверный файл резервной копии + + + Фильтр хостов + + + Активный + + + Источник файлов Geo (необязательно) + + + Источник файлов наборов правил sing-box (необязательно) + + + Программы для обновления не существует + + + Источник правил маршрутизации + + + Региональные пресеты + + + По умолчанию (Китай) + + + Россия + + + Иран + + + Пользователи из Китая могут пропустить этот пункт + + + Сканировать QR-код с изображения + + + Неверный адрес (Url) + + + Не используйте небезопасный адрес подписки по протоколу HTTP + + + Установите шрифт в систему и перезапустите настройки + + + Вы уверены, что хотите выйти? + + + Заметка (Memo) + + + Пароль sudo системы + + + Пароль sudo будет проверен в терминале. Если из-за ошибки проверки приложение начнёт работать некорректно, перезапустите его. Пароль не сохраняется — его нужно вводить после каждого перезапуска. + + + *XHTTP-режим + + + Дополнительный „сырой“ JSON для XHTTP, формат: { XHTTP Object } + + + Скрыть в трее при закрытии окна + + + Количество одновременно выполняемых тестов при многоэтапном тестировании + + + Исключение. Не используйте прокси-сервер для адресов с запятой (,) + + + Тип сниффинга + + + Включить второй смешанный порт + + + socks: локальный порт, socks2: второй локальный порт, socks3: LAN порт + + + Темы + + + Копировать команду прокси в буфер обмена + + + Повторное тестирование неудачных элементов, осталось {0}. Нажмите ESC для остановки… + + + По результату теста + + + Удалить недействительные по результатам теста + + + Удалено {0} недействительных + + + Диапазон портов сервера + + + Заменит указанный порт, перечисляйте через запятую (,) + + + От мультиконфигурации к пользовательской конфигурации + + + Случайный (Xray) + + + Круговой (Xray) + + + Минимальное RTT (минимальное время туда-обратно) (Xray) + + + Минимальная нагрузка (Xray) + + + Минимальное RTT (минимальное время туда-обратно) (sing-box) + + + Экспортировать конфигурацию + + + URL для тестирования текущего соединения + + + Можно указать название (Remarks) из конфигурации, убедитесь, что оно существует и уникально + + + Неверный пароль, попробуйте ещё раз. + + + Mldsa65Verify + + + Добавить сервер [Anytls] + + + Удалённый DNS + + + Внутренний DNS + + + Резолвер DNS для исходящих (sing-box) + + + Разрешать домены для исходящих соединений + + + Сервер DoH-резолвера (sing-box) + + + Резервное DNS-разрешение (рекомендуется указывать IP) + + + Стратегия резолвинга Freedom (Xray) + + + Стратегия прямого резолвинга (sing-box) + + + Стратегия удалённого резолвинга (sing-box) + + + Добавить стандартные записи hosts (DNS) + + + Сервер DoH-резолвера sing-box можно переопределить + + + FakeIP + + + Блокировать DNS-запросы SVCB и HTTPS + + + DNS hosts: (каждая строка в формате "domain1 ip1 ip2") + + + Применять только к доменам через прокси + + + Базовые настройки DNS + + + Расширенные настройки DNS + + + Проверять IP-адреса региональных доменов + + + При включении проверяет IP-адреса, возвращаемые для региональных доменов (например, geosite:cn), и оставляет только ожидаемые IP-адреса + + + Включить пользовательский DNS + + + Включён пользовательский DNS — настройки на этой странице не применяются + + + Предотвращает сбои доменных правил маршрутизации + + + Пожалуйста, заполните корректный шаблон конфигурации + + + Настройка полного шаблона конфигурации + + + Включить полный шаблон конфигурации + + + Полный шаблон конфигурации v2ray + + + Добавляет только конфигурацию исходящих (outbound), а также routing.balancers и routing.rules.outboundTag. Нажмите, чтобы открыть документ + + + Не добавлять исходящие для непрокси-протоколов + + + Задать тег верхнего прокси (upstream) + + + Полный шаблон конфигурации sing-box + + + Добавляет только конфигурацию Outbound и Endpoint. Нажмите, чтобы открыть документ + + + Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную. + + + Start parsing and processing subscription content + + + Select Profile + + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx new file mode 100644 index 00000000..194a59e2 --- /dev/null +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -0,0 +1,1515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 导出分享链接至剪贴板成功 + + + 请先检查配置文件设置 + + + 配置格式不正确 + + + 注意,自定义配置完全依赖您自己的配置,不能使用所有设置功能。如需使用系统代理请手动修改监听端口。 + + + 下载开始... + + + 转换配置文件失败 + + + 生成默认配置文件失败 + + + 获取默认配置失败 + + + 导入自定义配置文件失败 + + + 读取配置文件失败 + + + 请填写正确格式的端口 + + + 请填写本地监听端口 + + + 请填写密码 + + + 请填写地址 + + + 请填写用户 ID + + + 配置不正确,请检查 + + + 初始化配置 + + + {0} {1} 已是最新版本。 + + + {0} {1} 已是最新版本。 + + + 地址 + + + 加密方式 + + + 端口 + + + 类型 + + + 订阅分组 + + + 今日下载 + + + 今日上传 + + + 总下载 + + + 总上传 + + + 传输协议 + + + 下载 Core 成功 + + + 导入订阅内容失败 + + + 获取订阅内容成功 + + + 未设置有效的订阅 + + + 解析 {0} 成功 + + + 开始获取订阅内容 + + + 开始更新 {0}... + + + 无效的订阅内容 + + + 正在解压...... + + + 更新订阅结束 + + + 更新订阅开始 + + + 更新 Core 成功 + + + 更新 Core 成功!正在重启服务... + + + 非 VMess 或 ss 协议 + + + 在文件夹 ({0}) 下未找到 Core 文件 (文件名: {1}),请下载后放入文件夹,下载地址: {2} + + + 扫描完成,未发现有效二维码 + + + 操作失败,请检查并重试 + + + 请填写别名 + + + 请选择加密方式 + + + 请选择协议 + + + 请先选择配置文件 + + + 配置文件去重完成。原数量: {0},现数量: {1}。 + + + 是否确定移除配置文件? + + + 客户端配置文件保存在:{0} + + + 启动服务 ({0})... + + + 配置成功。 {0} + + + 成功导入自定义配置文件 + + + 成功从剪贴板导入 {0} 个配置文件 + + + 扫描导入分享链接成功 + + + 当前延迟: {0} ms,{1} + + + 操作成功 + + + 请先选择规则 + + + 是否确定移除规则? + + + {0},必填其中一项. + + + 别名 + + + 可选地址 (Url) + + + 数量 + + + 请填写 Url + + + 是否追加规则?选择"是"则追加,选择"否"则全部替换。 + + + 下载 GeoFile:{0} 成功 + + + 信息 + + + 自定义图标 + + + 请填写正确的自定义 DNS + + + *ws/httpupgrade/xhttp path + + + *h2 path + + + *QUIC 加密密钥 + + + *grpc serviceName + + + *http host 中间逗号 (,) 分隔 + + + *ws/httpupgrade/xhttp host + + + *h2 host 中间逗号 (,) 分隔 + + + *QUIC 加密方式 + + + *tcp 伪装类型 + + + *kcp 伪装类型 + + + *QUIC 伪装类型 + + + *grpc 模式 + + + TLS + + + *Kcp seed + + + 注册全局热键 {0} 失败,原因:{1} + + + 注册全局热键 {0} 成功 + + + 所有 + + + 请浏览导入配置文件配置 + + + 测试中... + + + 局域网 + + + 本地 + + + 配置文件过滤器,按回车执行 + + + 检查更新 + + + 关闭 + + + 退出 + + + 全局热键设置 + + + 帮助 + + + 参数设置 + + + 推广 + + + 重启服务 + + + 路由设置 + + + 配置文件 + + + 设置 + + + 更新当前订阅 (不通过代理) + + + 更新当前订阅 (通过代理) + + + 订阅分组 + + + 订阅分组设置 + + + 更新全部订阅 (不通过代理) + + + 更新全部订阅 (通过代理) + + + 系统代理 + + + 清除系统代理 + + + 不改变系统代理 + + + Pac 模式 + + + 自动配置系统代理 + + + 颜色 + + + 语言 (需重启) + + + 从剪贴板导入分享链接 (Ctrl+V) + + + 扫描屏幕上的二维码 (Ctrl+S) + + + 克隆所选配置文件 + + + 移除重复的配置文件 + + + 移除所选配置文件 (多选) (Delete) + + + 设为活动配置文件 (Enter) + + + 清除所有服务统计数据 + + + 测试配置文件真连接延迟 (多选) (Ctrl+R) + + + 按测试结果排序 + + + 测试配置文件速度 (多选) (Ctrl+T) + + + 测试配置文件延迟 Tcping (多选) (Ctrl+O) + + + 导出所选配置文件完整配置 + + + 导出分享链接至剪贴板 (多选) (Ctrl+C) + + + 添加自定义配置文件 + + + 添加 [Shadowsocks] 配置文件 + + + 添加 [SOCKS] 配置文件 + + + 添加 [Trojan] 配置文件 + + + 添加 [VLESS] 配置文件 + + + 添加 [VMess] 配置文件 + + + 全选 (Ctrl+A) + + + 清除所有 + + + 复制 (Ctrl+C) + + + 复制所有 + + + 全选 (Ctrl+A) + + + 添加 + + + 删除 + + + 编辑 + + + 分享 + + + 启用更新 + + + 排序 + + + User Agent (可选) + + + 取消 + + + 确定 + + + 底层传输方式 (transport) + + + 地址 (address) + + + 跳过证书验证 (allowInsecure) + + + Alpn + + + 额外 ID (alterId) + + + Fingerprint + + + 伪装类型 (type) + + + 用户 ID (id) + + + 传输协议 (network) + + + 路径 (path) + + + 端口 (port) + + + 别名 (remarks) + + + 伪装域名 (host) + + + 加密方式 (security) + + + SNI + + + 传输层安全 (TLS) + + + *默认 tcp,选错会无法连接 + + + Core 类型 + + + 流控 (flow) + + + 生成 + + + 密码 (password) + + + 密码 (可选) + + + 用户 ID (id) + + + 加密方式 (encryption) + + + 用户名 (可选) + + + 加密方式 (encryption) + + + Socks 端口 + + + *自定义配置的 Socks 端口值,可不设置;当设置此值后,将使用 Xray/sing-box (Tun) 额外启动一个前置 Socks 服务,提供分流和速度显示等功能 + + + 浏览 + + + 编辑 + + + 高级代理设置,协议选择 (可选) + + + 允许来自局域网的连接 + + + 启动后隐藏窗口 + + + 自动更新 Geo 文件的间隔 (小时) + + + Core: 基础设置 + + + v2ray DNS 设置 + + + Core: KCP 设置 + + + Core 类型设置 + + + 默认跳过证书验证 (allowInsecure) + + + Outbound Freedom domainStrategy + + + 自动调整配置文件列宽在更新订阅后 + + + 检查 Pre-Release 更新 (请谨慎启用) + + + 例外 + + + 例外:对于下列字符开头的地址,不使用代理配置文件。使用分号 (;) 分隔。 + + + 显示实时速度 (需重启) + + + 去重时保留序号较小的项 + + + 启用日志 + + + 日志等级 + + + 开启 Mux 多路复用 + + + v2rayN 设置 + + + 认证密码 + + + 自定义 DNS (可多个,用逗号 (,) 分隔) + + + 解除 Win10 UWP 应用回环代理限制 + + + 开启流量探测 + + + 本地混合监听端口 + + + 开机启动 (可能会不成功) + + + 启用流量统计 (需重启) + + + 订阅转换网址 (可选) + + + 系统代理设置 + + + 启用安全协议 TLS v1.3 (订阅/检查更新) + + + 托盘右键菜单配置文件展示数量限制 + + + 开启 UDP + + + 认证用户名 + + + 清除系统代理 + + + 显示主界面 + + + 全局热键设置 + + + 直接按键盘进行设置,重启后生效 + + + 不改变系统代理 + + + 重置 + + + 自动配置系统代理 + + + Pac 模式 + + + 分享配置文件 (Ctrl+F) + + + 路由 + + + 以非管理员身份运行 + + + 以管理员身份运行 + + + 下移至底 (B) + + + 下移 (D) + + + 上移至顶 (T) + + + 上移 (U) + + + 过滤器 (支持正则) + + + {0} 官网 + + + 添加规则集 + + + 一键导入规则集 + + + 移除所选规则 (Delete) + + + 设为活动规则 (Enter) + + + 域名解析策略 + + + 预定义规则集列表 + + + *设置的路由规则,用逗号 (,) 分隔;正则中的逗号用 <COMMA> 替代 + + + 从剪贴板中导入规则 + + + 从文件中导入规则 + + + 从订阅 Url 中导入规则 + + + 规则集设置 + + + 添加规则 + + + 导出所选规则至剪贴板 + + + 规则列表 + + + 移除所选规则 (Delete) + + + 路由规则详情设置 + + + 保存时 Domain, IP, 进程名 自动排序 + + + 规则详细说明文档 + + + 支持填写 DnsObject,JSON 格式,点击查看文档 + + + 普通分组此处请留空 + + + 路由设置改变 + + + 系统代理设置改变 + + + 仅限路由 (routeOnly) + + + 请勿将代理服务器用于本地 (Intranet) 地址 + + + 一键多线程测试延迟和速度 (Ctrl+E) + + + 延迟 (ms) + + + 速度 (M/s) + + + 运行 Core 失败,请查看提示信息 + + + 别名正则过滤 + + + 显示日志 + + + 启用 Tun + + + 为局域网开启新的端口 + + + Tun 模式设置 + + + 移至订阅分组 + + + 启用配置文件拖放排序 (需重启) + + + 自动刷新 + + + 跳过测试 + + + 编辑配置文件 (Ctrl+D) + + + 主界面双击设为活动配置文件 + + + 测试完成 + + + 默认 TLS 指纹 (fingerprint) + + + 用户代理 (User-Agent) + + + 仅对 tcp/http、ws 协议生效 + + + 当前字体 (需重启) + + + 拷贝字体 TTF/TTC 文件到目录 guiFonts,重启生效 + + + Pac 端口 = +3;Xray API 端口 = +4;mihomo API 端口 = +5; + + + 以管理员权限设置此项,在启动后获得管理员权限 + + + 字体大小 + + + 测速单个超时值 + + + 测速文件地址 + + + 移至上下 + + + PublicKey + + + ShortId + + + SpiderX + + + 启用硬件加速 (需重启) + + + 等待测试中 (按 ESC 终止)... + + + 当有异常断流时请关闭 + + + 未启用更新,跳过此订阅 + + + 以管理员身份重启 + + + 更多地址 (url),用逗号 (,) 分隔;订阅转换将失效 + + + 自动更新间隔 (分钟) + + + 启用日志存到文件 + + + 订阅转换目标类型 + + + 不需要转换时请留空 + + + DNS 设置 + + + sing-box DNS 设置 + + + 请填写 DNS JSON 结构,点击查看文档 + + + 点击导入默认 DNS 配置 + + + sing-box 域名解析策略 + + + sing-box Mux 多路复用协议 + + + 进程名全称 (Tun 模式) + + + IP 或 IP CIDR + + + Domain + + + 添加 [Hysteria2] 配置文件 + + + Hysteria 最大带宽 (Up/Dw) + + + 使用系统 hosts + + + 添加 [TUIC] 配置文件 + + + 拥塞控制算法 + + + 前置代理配置文件别名 + + + 落地代理配置文件別名 + + + 请确保配置文件别名存在并唯一 + + + 自动路由 + + + 严格路由 + + + 协议栈 + + + MTU + + + 启用额外监听端口 + + + 启用 IPv6 + + + 添加 [WireGuard] 配置文件 + + + PrivateKey + + + Reserved (2,3,4) + + + Address (IPv4,IPv6) + + + 混淆密码 (obfs password) + + + (Domain 或 IP 或 进程名) 与 Port 与 Protocol 与 InboundTag => OutboundTag + + + 自动滚动到末尾 + + + 真连接测试地址 + + + 更新订阅时只判断别名已存在否 + + + 测试终止中... + + + *grpc Authority + + + 添加 [HTTP] 配置文件 + + + 和分组前置代理冲突 + + + 启用分片 (Fragment) + + + 启用 sing-box (规则集文件) 的缓存文件 + + + 自定义 sing-box rule-set + + + 操作成功。请点击设置菜单重启应用。 + + + 打开存储所在的位置 + + + 排序 + + + 路由链 + + + 默认 + + + 延迟 + + + 下载速度 + + + 下载流量 + + + 主机 + + + 名称 + + + 网络 + + + 时间 + + + 类型 + + + 上传速度 + + + 上传流量 + + + 当前连接 + + + 关闭连接 + + + 关闭所有连接 + + + 当前代理 + + + 规则模式 + + + 直连 + + + 全局 + + + 随原配置 + + + 规则 + + + 延迟测试 + + + 当前部分节点延迟测试 + + + 刷新 + + + 设为活动节点 (Enter) + + + Outbound 默认解析策略 + + + 主界面布局方向 (需重启) + + + Outbound 域名解析地址 + + + 自动调整列宽 + + + 导出分享链接至剪贴板 (多选) Base64 编码 + + + 导出所选配置文件完整配置至剪贴板 + + + 显示或隐藏主界面 + + + 自定义配置的 Socks 端口 + + + 备份和还原 + + + 备份到本地 + + + 从本地恢复 + + + 备份到远程 (WebDAV) + + + 从远程恢复 (WebDAV) + + + 本地 + + + 远程 (WebDAV) + + + WebDav 服务器地址 + + + WebDav 账户 + + + WebDav 密码 + + + WebDav 可用检查 + + + 远程文件夹名称 (可选) + + + 无效备份文件 + + + 主机过滤器 + + + 活动 + + + Geo 文件来源 (可选) + + + sing-box ruleset 文件来源 (可选) + + + 升级工具 App 不存在 + + + 路由规则集来源 (可选) + + + 区域预置设置 + + + 默认区域 + + + 俄罗斯 + + + 伊朗 + + + 中国区域用户可忽略此项 + + + 扫描图片中的二维码 + + + 地址 (Url) 无效 + + + 请不要使用不安全的 HTTP 协议订阅地址 + + + 安装字体到系统中,选择或填入字体名称,重启生效 + + + 是否确定退出? + + + 备注备忘 + + + 系统的 sudo 密码 + + + 密码将调用命令行校验,如果因为校验错误导致无法正常运行时,请重启本应用。 密码不会存储,每次重启后都需要再次输入。 + + + *XHTTP 模式 + + + XHTTP Extra 原始 JSON,格式: { XHTTPObject } + + + 关闭窗口时隐藏至托盘 + + + 多线程测试时的并发数量 + + + 例外:对于下列地址不使用代理配置文件。使用逗号 (,) 分隔。 + + + 流量探测类型 + + + 开启第二个本地监听端口 + + + Socks:本地端口,Socks2:第二个本地端口,Socks3:局域网端口 + + + 主题 + + + 复制终端代理命令至剪贴板 + + + 开始对失败部分进行重新测试,剩余 {0} 个。可按 ESC 终止... + + + 按测试结果 + + + 按测试结果移除无效 + + + 移除无效测试结果 {0} 个。 + + + 跳跃端口范围 + + + 会覆盖端口,多组时用逗号 (,) 隔开 + + + 多配置文件产生自定义配置 (多选) + + + 多配置文件随机 Xray + + + 多配置文件负载均衡 Xray + + + 多配置文件最低延迟 Xray + + + 多配置文件最稳定 Xray + + + 多配置文件最低延迟 sing-box + + + 导出配置文件 + + + 当前连接信息测试地址 + + + 可以填写配置文件别名,请确保存在并唯一 + + + 密码错误,请重试。 + + + Mldsa65Verify + + + 添加 [Anytls] 配置文件 + + + 远程 DNS + + + 直连 DNS + + + 出站 DNS 解析(sing-box) + + + 解析出站域名 + + + sing-box DoH 解析服务器 + + + 兜底解析其他 DNS 域名,建议设为 ip + + + xray freedom 解析策略 + + + sing-box 直连解析策略 + + + sing-box 远程解析策略 + + + 添加常用 DNS Hosts + + + 开启后可覆盖 sing-box DoH 解析服务器 + + + FakeIP + + + 阻止 SVCB 和 HTTPS 查询 + + + DNS Hosts:(“域名1 ip1 ip2” 一行一个) + + + 仅对代理域名生效 + + + DNS 基础设置 + + + DNS 进阶设置 + + + 校验相应地区域名 IP + + + 配置后,会对相应地区域名(如 geosite:cn)的返回 IP 进行校验,仅返回期望 IP + + + 启用自定义 DNS + + + 自定义 DNS 已启用,此页面配置将无效 + + + 避免域名分流规则失效 + + + 请填写正确的配置模板 + + + 完整配置模板设置 + + + 启用完整配置模板 + + + v2ray 完整配置模板 + + + 仅添加出站配置,routing.balancers 和 routing.rules.outboundTag,点击查看文档 + + + 不添加非代理协议出站 + + + 设置上游代理 tag + + + sing-box 完整配置模板 + + + 仅添加出站和端点配置,点击查看文档 + + + 此功能供高级用户和有特殊需求的用户使用。 启用此功能后,将忽略 Core 的基础设置,DNS 设置 ,路由设置。你需要保证系统代理的端口和流量统计等功能的配置正确,一切都由你来设置。 + + + 开始解析和处理订阅内容 + + + 选择配置文件 + + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx new file mode 100644 index 00000000..fa84c789 --- /dev/null +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -0,0 +1,1515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 匯出分享連結至剪貼簿成功 + + + 請先檢查設定檔設定 + + + 設定格式不正確 + + + 注意,自訂設定完全依賴您自己的設定,不能使用所有設定功能。如需使用系統代理請手動修改偵聽埠。 + + + 下載開始... + + + 轉換設定檔失敗 + + + 生成預設設定檔失敗 + + + 獲取預設設定失敗 + + + 匯入自訂設定設定檔失敗 + + + 讀取設定檔失敗 + + + 請填寫正確格式的埠 + + + 請填寫本機偵聽埠 + + + 請填寫密碼 + + + 請填寫位址 + + + 請填寫使用者 ID + + + 設定不正確,請檢查 + + + 初始化設定 + + + {0} {1} 已是最新版本。 + + + {0} {1} 已是最新版本。 + + + 位址 + + + 加密方式 + + + + + + 類型 + + + 訂閱分組 + + + 今日下載 + + + 今日上傳 + + + 總下載 + + + 總上傳 + + + 傳輸協定 + + + 下載 Core 成功 + + + 匯入訂閱內容失敗 + + + 獲取訂閱內容成功 + + + 未設定有效的訂閱 + + + 解析 {0} 成功 + + + 開始獲取訂閱內容 + + + 開始更新 {0}... + + + 無效的訂閱內容 + + + 正在解壓...... + + + 更新訂閱結束 + + + 更新訂閱開始 + + + 更新 Core 成功 + + + 更新 Core 成功!正在重啟服務... + + + 非 VMess 或 SS 協定 + + + 在資料夾 ({0}) 下未找到 Core 檔案 (檔案名: {1}),請下載後放入資料夾,下載網址: {2} + + + 掃描完成,未發現有效二維碼 + + + 操作失敗,請檢查後重試 + + + 請填寫別名 + + + 請選擇加密方式 + + + 請選擇協定 + + + 請先選擇設定檔 + + + 設定檔去重完成。原數量: {0},現數量: {1}。 + + + 是否確定移除設定檔? + + + 用戶端設定檔儲存在:{0} + + + 啟動服務 ({0})... + + + 設定成功。{0} + + + 成功匯入自訂設定設定檔 + + + 成功從剪貼簿匯入 {0} 個設定檔 + + + 掃描匯入分享連結成功 + + + 目前延遲: {0} ms,{1} + + + 操作成功 + + + 請先選擇規則 + + + 是否確定移除規則? + + + {0},必填其中一項. + + + 別名 + + + 可選位址 (URL) + + + 數量 + + + 請填寫 URL + + + 是否追加規則?選擇"是"則追加,選擇"否"則完全取代。 + + + 下載 GeoFile:{0} 成功 + + + 資訊 + + + 自訂圖示 + + + 請填寫正確的自訂 DNS + + + *ws/httpupgrade/xhttp path + + + *h2 path + + + *QUIC 加密金鑰 + + + *grpc serviceName + + + *http host 中間逗號 (,) 分隔 + + + *ws/httpupgrade/xhttp host + + + *h2 host 中間逗號 (,) 分隔 + + + *QUIC 加密方式 + + + *TCP 偽裝類型 + + + *KCP 偽裝類型 + + + *QUIC 偽裝類型 + + + *GRPC 模式 + + + TLS + + + *KCP seed + + + 註冊全域快速鍵 {0} 失敗,原因:{1} + + + 註冊全域快速鍵 {0} 成功 + + + 所有 + + + 請瀏覽匯入設定檔設定 + + + 測試中... + + + 區域網路 + + + 本機 + + + 設定檔過濾,按 Enter 執行 + + + 檢查更新 + + + 關閉 + + + 退出 + + + 全域快速鍵設定 + + + 說明 + + + 參數設定 + + + 推廣 + + + 重啟服務 + + + 路由設定 + + + 設定檔 + + + 設定 + + + 更新目前訂閱 (不透過代理) + + + 更新目前訂閱 (透過代理) + + + 訂閱分組 + + + 訂閱分組設定 + + + 更新全部訂閱 (不透過代理) + + + 更新全部訂閱 (透過代理) + + + 系統代理 + + + 清除系統代理 + + + 不改變系統代理 + + + PAC 模式 + + + 自動設定系統代理 + + + 顏色 + + + 語言 (需重啟) + + + 從剪貼簿導入分享連結 (Ctrl+V) + + + 掃描螢幕上的二維碼 (Ctrl+S) + + + 複製所選設定檔 + + + 移除重複的設定檔 + + + 移除所選設定檔 (多選) (Delete) + + + 設為活動設定檔 (Enter) + + + 清除所有服務統計資料 + + + 測試設定檔真連線延遲 (多選) (Ctrl+R) + + + 按測試結果排序 + + + 測試設定檔速度 (多選) (Ctrl+T) + + + 測試設定檔延遲 Tcping (多選) (Ctrl+O) + + + 匯出所選設定檔完整設定 + + + 匯出分享連結至剪貼簿 (多選) (Ctrl+C) + + + 新增自訂設定設定檔 + + + 新增 [Shadowsocks] 設定檔 + + + 新增 [SOCKS] 設定檔 + + + 新增 [Trojan] 設定檔 + + + 新增 [VLESS] 設定檔 + + + 新增 [VMess] 設定檔 + + + 全選 (Ctrl+A) + + + 清除所有 + + + 複製 (Ctrl+C) + + + 複製所有 + + + 全選 (Ctrl+A) + + + 新增 + + + 刪除 + + + 編輯 + + + 分享 + + + 啟用更新 + + + 排序 + + + User Agent (可選) + + + 取消 + + + 確定 + + + 底層傳輸方式 (transport) + + + 位址 (address) + + + 跳過憑證驗證 (allowinsecure) + + + ALPN + + + 額外 ID (alterid) + + + Fingerprint + + + 偽裝類型 (type) + + + 使用者 ID (id) + + + 傳輸協定 (network) + + + 路徑 (path) + + + 埠 (port) + + + 別名 (remarks) + + + 偽裝域名 (host) + + + 加密方式 (security) + + + SNI + + + 傳輸層安全 (TLS) + + + *預設 TCP,選錯會無法連接 + + + Core 類型 + + + 流控 (flow) + + + 生成 + + + 密碼 (password) + + + 密碼 (可選) + + + 使用者 ID (id) + + + 加密方式 (encryption) + + + 使用者名稱 (可選) + + + 加密方式 (encryption) + + + SOCKS 埠 + + + *自訂設定的 Socks 埠值,可不設定;當設定此值後,將使用 Xray/sing-box (Tun) 額外啟動一個前置 Socks 服務,提供分流和速度顯示等功能 + + + 瀏覽 + + + 編輯 + + + 進階代理設定,協定選擇 (可選) + + + 允許來自區域網路的連線 + + + 啟動後隱藏視窗 + + + 自動更新 Geo 檔案的間隔 (小時) + + + Core: 基礎設定 + + + V2ray DNS 設定 + + + Core: KCP 設定 + + + Core 類型設定 + + + 預設跳過憑證驗證 (allowinsecure) + + + Outbound Freedom domainStrategy + + + 在更新訂閱後自動調整設定檔列寬 + + + 檢查 Pre-Release 更新 (請謹慎啟用) + + + 例外 + + + 例外:對於下列字元開頭的位址,不使用代理設定檔。使用分號 (;) 分隔。 + + + 顯示即時速度(需重啟) + + + 去重時保留序號較小的項 + + + 啟用日誌 + + + 日誌等級 + + + 開啟 Mux 多路復用 + + + v2rayN 設定 + + + 認證密碼 + + + 自訂 DNS (可多個,用逗號 (,) 分隔) + + + 解除 Win10 UWP 應用回環代理限制 + + + 開啟流量探測 + + + 本機混合偵聽埠 + + + 開機啟動 (可能會不成功) + + + 啟用流量統計(需重啟) + + + 訂閱轉換網址 (可選) + + + 系統代理設定 + + + 啟用安全協定 TLS v1.3 (訂閱/檢查更新) + + + 工具列右鍵選單設定檔展示數量限制 + + + 開啟 UDP + + + 認證使用者名稱 + + + 清除系統代理 + + + 顯示主介面 + + + 全域快速鍵設定 + + + 直接按鍵盤進行設定,重啟後生效 + + + 不改變系統代理 + + + 重設 + + + 自動設定系統代理 + + + PAC 模式 + + + 分享設定檔 (Ctrl+F) + + + 路由 + + + 以非管理員身份執行 + + + 以管理員身份執行 + + + 下移至底部 (B) + + + 下移 (D) + + + 上移至頂部 (T) + + + 上移 (U) + + + 過濾 (允許正則) + + + {0} 官網 + + + 新增規則集 + + + 一鍵匯入規則集 + + + 移除所選規則 (Delete) + + + 設為活動規則 (Enter) + + + 域名解析策略 + + + 預定義規則集列表 + + + *設定的路由規則,用逗號 (,) 分隔;正則中的逗號用 <COMMA> 替代 + + + 從剪貼簿中匯入規則 + + + 從檔案中匯入規則 + + + 從訂閱 URL 中匯入規則 + + + 規則集設定 + + + 新增規則 + + + 匯出所選規則至剪貼簿 + + + 規則列表 + + + 移除所選規則 (Delete) + + + 路由規則詳情設定 + + + 儲存時 Domain, IP, 行程名 自動排序 + + + 規則詳細說明檔案 + + + 支援填寫 DnsObject,JSON 格式,點擊查看說明 + + + 普通分組此處請留空 + + + 路由設定已改變 + + + 系統代理設定已改變 + + + 僅限路由 (routeOnly) + + + 請勿將代理伺服器用於本機(Intranet)位址 + + + 一鍵多執行緒測試延遲和速度 (Ctrl+E) + + + 延遲 (ms) + + + 速度 (M/s) + + + 執行 Core 失敗,請查看提示訊息 + + + 別名正則過濾 + + + 顯示日誌 + + + 啟用 Tun + + + 為區域網路開啟新的埠 + + + Tun 模式設定 + + + 移至訂閱分組 + + + 啟動設定檔拖放排序 (需重啟) + + + 自動重新整理 + + + 跳過測試 + + + 編輯設定檔 (Ctrl+D) + + + 主介面輕按兩下設為活動設定檔 + + + 測試完成 + + + 預設 TLS 指紋 (fingerprint) + + + 使用者代理 (User-Agent) + + + 僅對 TCP/HTTP、WS 協定生效 + + + 目前字型 (需重啟) + + + 複製字型 TTF/TTC 檔案到目錄 guiFonts,重啟設定 + + + Pac 連接埠 = +3;Xray API 連接埠 = +4;mihomo API 連接埠 = +5; + + + 以管理員權限設定此項,在啟動後獲得管理員權限 + + + 字型大小 + + + 測速單個超時值 + + + 測速檔案位址 + + + 移至上下 + + + PublicKey + + + ShortId + + + SpiderX + + + 啟用硬體加速 (需重啟) + + + 等待測試中(按 ESC 終止)... + + + 當有異常斷流時請關閉 + + + 未啟動更新,跳過此訂閱 + + + 以管理員身份重啟 + + + 更多位址 (url),用逗號 (,) 分隔;訂閱轉換將失效 + + + 自動更新間隔 (分鐘) + + + 啟動日誌存到檔案 + + + 訂閱轉換目標類型 + + + 不需要轉換時請留空 + + + DNS 設定 + + + sing-box DNS 設定 + + + 請填寫 DNS JSON 結構,點擊查看檔案 + + + 點擊匯入預設 DNS 設定 + + + sing-box 域名解析策略 + + + sing-box Mux 多路復用協定 + + + 行程名全稱 (Tun 模式) + + + IP 或 IP CIDR + + + Domain + + + 添加 [Hysteria2] 設定檔 + + + Hysteria 最大頻寬 (Up/Dw) + + + 使用系統 hosts + + + 新增 [TUIC] 設定檔 + + + 擁塞控制算法 + + + 前置代理設定檔別名 + + + 落地代理設定檔別名 + + + 請確保設定檔別名存在並且唯一 + + + 自動路由 + + + 嚴格路由 + + + 協定堆疊 + + + MTU + + + 啟用額外偵聽連接埠 + + + 啟用 IPv6 + + + 添加 [WireGuard] 設定檔 + + + PrivateKey + + + Reserved (2,3,4) + + + Address (Ipv4,Ipv6) + + + 混淆密碼 (obfs password) + + + (Domain 或 IP 或 行程名) 與 Port 與 Protocol 與 InboundTag => OutboundTag + + + 自動滾動到末尾 + + + 真連線測試位址 + + + 更新訂閱時只判斷別名是否存在 + + + 測試終止中... + + + *grpc Authority + + + 新增 [HTTP] 設定檔 + + + 和分組前置代理衝突 + + + 啟用分片(Fragment) + + + 啟用 sing-box(規則集檔案)的快取檔案 + + + 自訂 sing-box rule-set + + + 操作成功。請點選設定選單重啟應用程式。 + + + 打開儲存所在的位置 + + + 排序 + + + 路由鏈 + + + 預設 + + + 延遲 + + + 下載速度 + + + 下載流量 + + + 主機 + + + 名稱 + + + 網路 + + + 時間 + + + 類型 + + + 上傳速度 + + + 上傳流量 + + + 目前連線 + + + 關閉連線 + + + 關閉所有連線 + + + 目前代理 + + + 規則模式 + + + 直連 + + + 全局 + + + 隨原配置 + + + 規則 + + + 延遲測試 + + + 目前部分節點延遲測試 + + + 重新整理 + + + 設為活動節點 (Enter) + + + Outbound 預設解析策略 + + + 主界面佈局方向 (需重啟) + + + Outbound 域名解析位址 + + + 自動調整列寬 + + + 匯出分享連結至剪貼簿 (多選) Base64 編碼 + + + 匯出所選設定檔完整設定至剪貼簿 + + + 顯示或隱藏主介面 + + + 自訂設定的 Socks 連接埠 + + + 備份和還原 + + + 備份到本地 + + + 從本地恢復 + + + 備份到遠端 (WebDAV) + + + 從遠端恢復 (WebDAV) + + + 本地 + + + 遠端 (WebDAV) + + + WebDav 伺服器位址 + + + WebDav 帳戶 + + + WebDav 密碼 + + + WebDav 可用檢查 + + + 遠端資料夾名稱 (可選) + + + 無效備份檔案 + + + 主機過濾 + + + 活動 + + + Geo 檔案來源 (可選) + + + sing-box ruleset 檔案來源 (可選) + + + 升級工具 App 不存在 + + + 路由規則集來源 (可選) + + + 區域預置設定 + + + 預設區域 + + + 俄羅斯 + + + 伊朗 + + + 中國區域用戶可忽略此項 + + + 掃描圖片中的二維碼 + + + 位址 (Url) 無效 + + + 請不要使用不安全的 HTTP 協定訂閱位址 + + + 安裝字體到系統中,選擇或填入字體名稱,重新啟動設定 + + + 是否確定退出? + + + 備註備忘 + + + 系統的 sudo 密碼 + + + 密碼將調用命令行校驗,如果因為校驗錯誤導致無法正常運行時,請重啟本應用。密碼不會存儲,每次重啟後都需要再次輸入。 + + + *xhttp 模式 + + + XHTTP Extra 原始 JSON,格式: { XHTTPObject } + + + 關閉視窗時隱藏至托盤 + + + 多執行緒測試時的並發數量 + + + 例外:對於下列位址不使用代理設定檔,使用逗號 (,) 分隔。 + + + 流量探測類型 + + + 開啟第二個本機監聽埠 + + + socks:本地埠,socks2:第二個本地埠,socks3:區域網路埠 + + + 主題 + + + 複製終端代理指令至剪貼簿 + + + 開始對失敗部分進行重新測試,剩餘 {0} 個。可按 ESC 終止... + + + 按測試結果 + + + 按測試結果移除無效 + + + 移除無效測試結果 {0} 個。 + + + 跳躍埠範圍 + + + 會覆蓋埠,多組時用逗號 (,) 隔開 + + + 多設定檔產生自訂配置 (多選) + + + 多設定檔隨機 Xray + + + 多設定檔負載平衡 Xray + + + 多設定檔最低延遲 Xray + + + 多設定檔最穩定 Xray + + + 多設定檔最低延遲 sing-box + + + 匯出設定檔 + + + 目前連接資訊測試地址 + + + 可以填寫設定檔別名,請確保存在並唯一 + + + 密碼錯誤,請重試。 + + + Mldsa65Verify + + + 新增 [Anytls] 設定檔 + + + Remote DNS + + + Domestic DNS + + + Outbound DNS Resolution (sing-box) + + + Resolve Outbound Domains + + + sing-box DoH Resolver Server + + + Fallback DNS Resolution, Suggest IP + + + xray Freedom Resolution Strategy + + + sing-box Direct Resolution Strategy + + + sing-box Remote Resolution Strategy + + + Add Common DNS Hosts + + + The sing-box DoH resolution server can be overwritten + + + FakeIP + + + Block SVCB and HTTPS Queries + + + DNS Hosts: ("domain1 ip1 ip2" per line) + + + Apply to Proxy Domains Only + + + Basic DNS Settings + + + Advanced DNS Settings + + + Validate Regional Domain IPs + + + When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs + + + Enable Custom DNS + + + Custom DNS Enabled, This Page's Settings Invalid + + + Prevent domain-based routing rules from failing + + + Please fill in the correct config template + + + Full Config Template Setting + + + Enable Full Config Template + + + v2ray Full Config Template + + + Add Outbound Config Only, routing.balancers and routing.rules.outboundTag, Click to view the document + + + Do Not Add Non-Proxy Protocol Outbound + + + Set Upstream Proxy Tag + + + sing-box Full Config Template + + + Add Outbound and Endpoint Config Only, Click to view the document + + + This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you. + + + 開始解析和處理訂閱內容 + + + Select Profile + + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/SampleClientConfig b/v2rayN/ServiceLib/Sample/SampleClientConfig new file mode 100644 index 00000000..407a1307 --- /dev/null +++ b/v2rayN/ServiceLib/Sample/SampleClientConfig @@ -0,0 +1,58 @@ +{ + "log": { + "access": "Vaccess.log", + "error": "Verror.log", + "loglevel": "warning" + }, + "inbounds": [], + "outbounds": [ + { + "tag": "proxy", + "protocol": "vmess", + "settings": { + "vnext": [{ + "address": "", + "port": 0, + "users": [{ + "id": "", + "security": "auto" + }] + }], + "servers": [{ + "address": "", + "method": "", + "ota": false, + "password": "", + "port": 0, + "level": 1 + }] + }, + "streamSettings": { + "network": "tcp" + }, + "mux": { + "enabled": false + } + }, + { + "protocol": "freedom", + "tag": "direct" + }, + { + "protocol": "blackhole", + "tag": "block" + } + ], + "routing": { + "domainStrategy": "IPIfNonMatch", + "rules": [ + { + "inboundTag": [ + "api" + ], + "outboundTag": "api", + "type": "field" + } + ] + } +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/SampleHttpRequest b/v2rayN/ServiceLib/Sample/SampleHttpRequest new file mode 100644 index 00000000..583b89eb --- /dev/null +++ b/v2rayN/ServiceLib/Sample/SampleHttpRequest @@ -0,0 +1 @@ +{"version":"1.1","method":"GET","path":[$requestPath$],"headers":{"Host":[$requestHost$],"User-Agent":[$requestUserAgent$],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}} \ No newline at end of file diff --git a/v2rayN/v2rayN/Sample/SampleHttpresponse.txt b/v2rayN/ServiceLib/Sample/SampleHttpResponse similarity index 100% rename from v2rayN/v2rayN/Sample/SampleHttpresponse.txt rename to v2rayN/ServiceLib/Sample/SampleHttpResponse diff --git a/v2rayN/ServiceLib/Sample/SampleInbound b/v2rayN/ServiceLib/Sample/SampleInbound new file mode 100644 index 00000000..a21452dd --- /dev/null +++ b/v2rayN/ServiceLib/Sample/SampleInbound @@ -0,0 +1,18 @@ +{ + "tag": "tag1", + "port": 10808, + "protocol": "socks", + "listen": "127.0.0.1", + "settings": { + "auth": "noauth", + "udp": true, + "allowTransparent": false + }, + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls" + ] + } +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/SampleOutbound b/v2rayN/ServiceLib/Sample/SampleOutbound new file mode 100644 index 00000000..54885375 --- /dev/null +++ b/v2rayN/ServiceLib/Sample/SampleOutbound @@ -0,0 +1,34 @@ +{ + "tag": "proxy", + "protocol": "vmess", + "settings": { + "vnext": [ + { + "address": "v2ray.cool", + "port": 10086, + "users": [ + { + "id": "a3482e88-686a-4a58-8126-99c9df64b7bf", + "security": "auto" + } + ] + } + ], + "servers": [ + { + "address": "v2ray.cool", + "method": "chacha20", + "ota": false, + "password": "123456", + "port": 10086, + "level": 1 + } + ] + }, + "streamSettings": { + "network": "tcp" + }, + "mux": { + "enabled": false + } +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/SingboxSampleClientConfig b/v2rayN/ServiceLib/Sample/SingboxSampleClientConfig new file mode 100644 index 00000000..b07fd72c --- /dev/null +++ b/v2rayN/ServiceLib/Sample/SingboxSampleClientConfig @@ -0,0 +1,23 @@ +{ + "log": { + "level": "debug", + "timestamp": true + }, + "inbounds": [], + "outbounds": [ + { + "type": "vless", + "tag": "proxy", + "server": "", + "server_port": 443 + }, + { + "type": "direct", + "tag": "direct" + } + ], + "route": { + "rules": [ + ] + } +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/SingboxSampleOutbound b/v2rayN/ServiceLib/Sample/SingboxSampleOutbound new file mode 100644 index 00000000..9c87194f --- /dev/null +++ b/v2rayN/ServiceLib/Sample/SingboxSampleOutbound @@ -0,0 +1,6 @@ +{ + "type": "vless", + "tag": "proxy", + "server": "", + "server_port": 443 +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/clash_mixin_yaml b/v2rayN/ServiceLib/Sample/clash_mixin_yaml new file mode 100644 index 00000000..940144ee --- /dev/null +++ b/v2rayN/ServiceLib/Sample/clash_mixin_yaml @@ -0,0 +1,39 @@ +# +# 配置文件内容不会被修改,混合行为只会发生在内存中 +# +# 注意下面缩进,请用支持yaml显示的编辑器打开 +# +# 使用clash配置文件关键字则覆盖原配置 +# +# removed-rules 循环匹配rules数组每行,符合则移除当前行 (此规则请放最前面) +# +# append-rules 数组合并至原配置rules数组后 +# prepend-rules 数组合并至原配置rules数组前 +# append-proxies 数组合并至原配置proxies数组后 +# prepend-proxies 数组合并至原配置proxies数组前 +# append-proxy-groups 数组合并至原配置proxy-groups数组后 +# prepend-proxy-groups 数组合并至原配置proxy-groups数组前 +# append-rule-providers 数组合并至原配置rule-providers数组后 +# prepend-rule-providers 数组合并至原配置rule-providers数组前 +# + +dns: + enable: true + enhanced-mode: fake-ip + nameserver: + - 114.114.114.114 + - 223.5.5.5 + - 8.8.8.8 + fallback: [] + fake-ip-filter: + - +.stun.*.* + - +.stun.*.*.* + - +.stun.*.*.*.* + - +.stun.*.*.*.*.* + - "*.n.n.srv.nintendo.net" + - +.stun.playstation.net + - xbox.*.*.microsoft.com + - "*.*.xboxlive.com" + - "*.msftncsi.com" + - "*.msftconnecttest.com" + - WORKGROUP \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/clash_tun_yaml b/v2rayN/ServiceLib/Sample/clash_tun_yaml new file mode 100644 index 00000000..d8427f33 --- /dev/null +++ b/v2rayN/ServiceLib/Sample/clash_tun_yaml @@ -0,0 +1,7 @@ +tun: + enable: true + stack: gvisor + dns-hijack: + - 0.0.0.0:53 + auto-route: true + auto-detect-interface: true diff --git a/v2rayN/ServiceLib/Sample/custom_routing_black b/v2rayN/ServiceLib/Sample/custom_routing_black new file mode 100644 index 00000000..ff2aa075 --- /dev/null +++ b/v2rayN/ServiceLib/Sample/custom_routing_black @@ -0,0 +1,149 @@ +[ + { + "remarks": "绕过bittorrent", + "outboundTag": "direct", + "protocol": [ + "bittorrent" + ] + }, + { + "remarks": "api.ip.sb", + "outboundTag": "proxy", + "domain": [ + "api.ip.sb" + ] + }, + { + "remarks": "Google cn", + "outboundTag": "proxy", + "domain": [ + "domain:googleapis.cn", + "domain:gstatic.com" + ] + }, + { + "remarks": "阻断udp443", + "outboundTag": "block", + "port": "443", + "network": "udp" + }, + { + "remarks": "绕过局域网IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + }, + { + "remarks": "绕过局域网域名", + "outboundTag": "direct", + "domain": [ + "geosite:private" + ] + }, + { + "remarks": "代理海外公共DNSIP", + "outboundTag": "proxy", + "ip": [ + "1.1.1.1", + "1.0.0.1", + "2606:4700:4700::1111", + "2606:4700:4700::1001", + "1.1.1.2", + "1.0.0.2", + "2606:4700:4700::1112", + "2606:4700:4700::1002", + "1.1.1.3", + "1.0.0.3", + "2606:4700:4700::1113", + "2606:4700:4700::1003", + "8.8.8.8", + "8.8.4.4", + "2001:4860:4860::8888", + "2001:4860:4860::8844", + "94.140.14.14", + "94.140.15.15", + "2a10:50c0::ad1:ff", + "2a10:50c0::ad2:ff", + "94.140.14.15", + "94.140.15.16", + "2a10:50c0::bad1:ff", + "2a10:50c0::bad2:ff", + "94.140.14.140", + "94.140.14.141", + "2a10:50c0::1:ff", + "2a10:50c0::2:ff", + "208.67.222.222", + "208.67.220.220", + "2620:119:35::35", + "2620:119:53::53", + "208.67.222.123", + "208.67.220.123", + "2620:119:35::123", + "2620:119:53::123", + "9.9.9.9", + "149.112.112.112", + "2620:fe::9", + "2620:fe::fe", + "9.9.9.11", + "149.112.112.11", + "2620:fe::11", + "2620:fe::fe:11", + "9.9.9.10", + "149.112.112.10", + "2620:fe::10", + "2620:fe::fe:10", + "77.88.8.8", + "77.88.8.1", + "2a02:6b8::feed:0ff", + "2a02:6b8:0:1::feed:0ff", + "77.88.8.88", + "77.88.8.2", + "2a02:6b8::feed:bad", + "2a02:6b8:0:1::feed:bad", + "77.88.8.7", + "77.88.8.3", + "2a02:6b8::feed:a11", + "2a02:6b8:0:1::feed:a11" + ] + }, + { + "remarks": "代理海外公共DNS域名", + "outboundTag": "proxy", + "domain": [ + "domain:cloudflare-dns.com", + "domain:one.one.one.one", + "domain:dns.google", + "domain:adguard-dns.com", + "domain:opendns.com", + "domain:umbrella.com", + "domain:quad9.net", + "domain:yandex.net" + ] + }, + { + "remarks": "代理IP", + "outboundTag": "proxy", + "ip": [ + "geoip:facebook", + "geoip:fastly", + "geoip:google", + "geoip:netflix", + "geoip:telegram", + "geoip:twitter" + ] + }, + { + "remarks": "代理GFW", + "outboundTag": "proxy", + "domain": [ + "geosite:gfw", + "geosite:greatfire" + ] + }, + { + "remarks": "最终直连", + "port": "0-65535", + "outboundTag": "direct" + } +] diff --git a/v2rayN/ServiceLib/Sample/custom_routing_global b/v2rayN/ServiceLib/Sample/custom_routing_global new file mode 100644 index 00000000..ef19e144 --- /dev/null +++ b/v2rayN/ServiceLib/Sample/custom_routing_global @@ -0,0 +1,27 @@ +[ + { + "remarks": "阻断udp443", + "outboundTag": "block", + "port": "443", + "network": "udp" + }, + { + "remarks": "绕过局域网IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + }, + { + "remarks": "绕过局域网域名", + "outboundTag": "direct", + "domain": [ + "geosite:private" + ] + }, + { + "remarks": "最终代理", + "port": "0-65535", + "outboundTag": "proxy" + } +] diff --git a/v2rayN/ServiceLib/Sample/custom_routing_white b/v2rayN/ServiceLib/Sample/custom_routing_white new file mode 100644 index 00000000..10dae092 --- /dev/null +++ b/v2rayN/ServiceLib/Sample/custom_routing_white @@ -0,0 +1,96 @@ +[ + { + "remarks": "Google cn", + "outboundTag": "proxy", + "domain": [ + "domain:googleapis.cn", + "domain:gstatic.com" + ] + }, + { + "remarks": "阻断udp443", + "outboundTag": "block", + "port": "443", + "network": "udp" + }, + { + "remarks": "绕过局域网IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + }, + { + "remarks": "绕过局域网域名", + "outboundTag": "direct", + "domain": [ + "geosite:private" + ] + }, + { + "remarks": "绕过中国公共DNSIP", + "outboundTag": "direct", + "ip": [ + "223.5.5.5", + "223.6.6.6", + "2400:3200::1", + "2400:3200:baba::1", + "119.29.29.29", + "1.12.12.12", + "120.53.53.53", + "2402:4e00::", + "2402:4e00:1::", + "180.76.76.76", + "2400:da00::6666", + "114.114.114.114", + "114.114.115.115", + "114.114.114.119", + "114.114.115.119", + "114.114.114.110", + "114.114.115.110", + "180.184.1.1", + "180.184.2.2", + "101.226.4.6", + "218.30.118.6", + "123.125.81.6", + "140.207.198.6", + "1.2.4.8", + "210.2.4.8", + "52.80.66.66", + "117.50.22.22", + "2400:7fc0:849e:200::4", + "2404:c2c0:85d8:901::4", + "117.50.10.10", + "52.80.52.52", + "2400:7fc0:849e:200::8", + "2404:c2c0:85d8:901::8", + "117.50.60.30", + "52.80.60.30" + ] + }, + { + "remarks": "绕过中国公共DNS域名", + "outboundTag": "direct", + "domain": [ + "domain:alidns.com", + "domain:doh.pub", + "domain:dot.pub", + "domain:360.cn", + "domain:onedns.net" + ] + }, + { + "remarks": "绕过中国IP", + "outboundTag": "direct", + "ip": [ + "geoip:cn" + ] + }, + { + "remarks": "绕过中国域名", + "outboundTag": "direct", + "domain": [ + "geosite:cn" + ] + } +] diff --git a/v2rayN/ServiceLib/Sample/dns_singbox_normal b/v2rayN/ServiceLib/Sample/dns_singbox_normal new file mode 100644 index 00000000..b32b439c --- /dev/null +++ b/v2rayN/ServiceLib/Sample/dns_singbox_normal @@ -0,0 +1,34 @@ +{ + "servers": [ + { + "tag": "remote", + "type": "tcp", + "server": "8.8.8.8", + "detour": "proxy" + }, + { + "tag": "local", + "type": "udp", + "server": "223.5.5.5" + } + ], + "rules": [ + { + "domain_suffix": [ + "googleapis.cn", + "gstatic.com" + ], + "server": "remote", + "strategy": "prefer_ipv4" + }, + { + "rule_set": [ + "geosite-cn" + ], + "server": "local", + "strategy": "prefer_ipv4" + } + ], + "final": "remote", + "strategy": "prefer_ipv4" +} diff --git a/v2rayN/ServiceLib/Sample/dns_v2ray_normal b/v2rayN/ServiceLib/Sample/dns_v2ray_normal new file mode 100644 index 00000000..fdb30dae --- /dev/null +++ b/v2rayN/ServiceLib/Sample/dns_v2ray_normal @@ -0,0 +1,29 @@ +{ + "hosts": { + "dns.google": "8.8.8.8", + "proxy.example.com": "127.0.0.1" + }, + "servers": [ + { + "address": "1.1.1.1", + "skipFallback": true, + "domains": [ + "domain:googleapis.cn", + "domain:gstatic.com" + ] + }, + { + "address": "223.5.5.5", + "skipFallback": true, + "domains": [ + "geosite:cn" + ], + "expectIPs": [ + "geoip:cn" + ] + }, + "1.1.1.1", + "8.8.8.8", + "https://dns.google/dns-query" + ] +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/kill_as_sudo_linux_sh b/v2rayN/ServiceLib/Sample/kill_as_sudo_linux_sh new file mode 100644 index 00000000..7f62a532 --- /dev/null +++ b/v2rayN/ServiceLib/Sample/kill_as_sudo_linux_sh @@ -0,0 +1,61 @@ +#!/bin/bash +# +# Process Terminator Script for Linux +# This script forcibly terminates a process and all its child processes +# + +# Check if PID argument is provided +if [ $# -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +PID=$1 + +# Validate that input is a valid PID (numeric) +if ! [[ "$PID" =~ ^[0-9]+$ ]]; then + echo "Error: The PID must be a numeric value" + exit 1 +fi + +# Check if the process exists +if ! ps -p $PID > /dev/null; then + echo "Warning: No process found with PID $PID" + exit 0 +fi + +# Recursive function to find and kill all child processes +kill_children() { + local parent=$1 + local children=$(ps -o pid --no-headers --ppid "$parent") + + # Output information about processes being terminated + echo "Processing children of PID: $parent..." + + # Process each child + for child in $children; do + # Recursively find and kill child's children first + kill_children "$child" + + # Force kill the child process + echo "Terminating child process: $child" + kill -9 "$child" 2>/dev/null || true + done +} + +echo "============================================" +echo "Starting termination of process $PID and all its children" +echo "============================================" + +# Find and kill all child processes +kill_children "$PID" + +# Finally kill the main process +echo "Terminating main process: $PID" +kill -9 "$PID" 2>/dev/null || true + +echo "============================================" +echo "Process $PID and all its children have been terminated" +echo "============================================" + +exit 0 diff --git a/v2rayN/ServiceLib/Sample/kill_as_sudo_osx_sh b/v2rayN/ServiceLib/Sample/kill_as_sudo_osx_sh new file mode 100644 index 00000000..94011d6f --- /dev/null +++ b/v2rayN/ServiceLib/Sample/kill_as_sudo_osx_sh @@ -0,0 +1,56 @@ +#!/bin/bash +# +# Process Terminator Script for macOS +# This script forcibly terminates a process and all its descendant processes +# + +# Check if PID argument is provided +if [ $# -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +PID=$1 + +# Validate that input is a valid PID (numeric) +if ! [[ "$PID" =~ ^[0-9]+$ ]]; then + echo "Error: The PID must be a numeric value" + exit 1 +fi + +# Check if the process exists - using kill -0 which is more reliable on macOS +if ! kill -0 $PID 2>/dev/null; then + echo "Warning: No process found with PID $PID" + exit 0 +fi + +# Recursive function to find and kill all descendant processes +kill_descendants() { + local parent=$1 + # Use ps -axo for macOS to ensure all processes are included + local children=$(ps -axo pid=,ppid= | awk -v ppid=$parent '$2==ppid {print $1}') + + echo "Processing children of PID: $parent..." + for child in $children; do + kill_descendants "$child" + echo "Terminating child process: $child" + kill -9 "$child" 2>/dev/null || true + done +} + +echo "============================================" +echo "Starting termination of process $PID and all its descendants" +echo "============================================" + +# Find and kill all descendant processes +kill_descendants "$PID" + +# Finally kill the main process +echo "Terminating main process: $PID" +kill -9 "$PID" 2>/dev/null || true + +echo "============================================" +echo "Process $PID and all its descendants have been terminated" +echo "============================================" + +exit 0 diff --git a/v2rayN/ServiceLib/Sample/linux_autostart_config b/v2rayN/ServiceLib/Sample/linux_autostart_config new file mode 100644 index 00000000..07eb0b27 --- /dev/null +++ b/v2rayN/ServiceLib/Sample/linux_autostart_config @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Exec=$ExecPath$ +Hidden=false +NoDisplay=false +X-GNOME-Autostart-enabled=true +Name[en_US]=v2rayN +Name=v2rayN +Comment[en_US]=v2rayN +Comment=v2rayN diff --git a/v2rayN/ServiceLib/Sample/pac b/v2rayN/ServiceLib/Sample/pac new file mode 100644 index 00000000..1378c49f --- /dev/null +++ b/v2rayN/ServiceLib/Sample/pac @@ -0,0 +1,6881 @@ +var proxy = '__PROXY__'; +var rules = [ + [ + [], + [ + "000webhost.com", + "030buy.com", + "0rz.tw", + "1-apple.com.tw", + "10.tt", + "1000giri.net", + "100ke.org", + "10beasts.net", + "10conditionsoflove.com", + "10musume.com", + "123rf.com", + "12bet.com", + "12vpn.com", + "12vpn.net", + "1337x.to", + "138.com", + "141-hk.com", + "141hongkong.com", + "141jj.com", + "141tube.com", + "1688.com.au", + "17.live", + "173ng.com", + "177pic.info", + "17t17p.com", + "18board.com", + "18board.info", + "18comic.org", + "18comic.vip", + "18hmanga.click", + "18jav.tv", + "18onlygirls.com", + "18p2p.com", + "18virginsex.com", + "1949er.org", + "1984.city", + "1984bbs.com", + "1984bbs.org", + "1991way.com", + "1998cdp.org", + "1bao.org", + "1dumb.com", + "1e100.net", + "1eew.com", + "1lib.domains", + "1mobile.com", + "1mobile.tw", + "1point3acres.com", + "1pondo.tv", + "2-hand.info", + "2000fun.com", + "2008xianzhang.info", + "2017.hk", + "2021hkcharter.com", + "2047.name", + "2047.one", + "2049bbs.xyz", + "21andy.com", + "21join.com", + "21pron.com", + "21sextury.com", + "228.net.tw", + "233abc.com", + "233v2.com", + "24hrs.ca", + "24smile.org", + "25u.com", + "2du5.com", + "2lipstube.com", + "2shared.com", + "2waky.com", + "3-a.net", + "30boxes.com", + "315lz.com", + "32red.com", + "36rain.com", + "3a5a.com", + "3arabtv.com", + "3boys2girls.com", + "3d-game.com", + "3proxy.ru", + "3ren.ca", + "3tui.net", + "404museum.com", + "43110.cf", + "466453.com", + "4bluestones.biz", + "4chan.com", + "4dq.com", + "4everproxy.com", + "4gtv.tv", + "4irc.com", + "4mydomain.com", + "4pu.com", + "4rbtv.com", + "4shared.com", + "4sqi.net", + "500px.com", + "500px.org", + "50webs.com", + "51.ca", + "51jav.org", + "51luoben.com", + "5278.cc", + "5299.tv", + "55comic.com", + "5aimiku.com", + "5i01.com", + "5isotoi5.org", + "5maodang.com", + "611.icu", + "611study.com", + "611study.icu", + "63i.com", + "64museum.org", + "64tianwang.com", + "64wiki.com", + "66.ca", + "666kb.com", + "666pool.cn", + "69shu.com", + "69shuba.cx", + "6do.news", + "6do.world", + "6park.com", + "6parkbbs.com", + "6parker.com", + "6parknews.com", + "7capture.com", + "7cow.com", + "7mmtv.tv", + "8-d.com", + "85cc.net", + "85cc.us", + "85st.com", + "881903.com", + "888.com", + "888poker.com", + "89-64.org", + "8964museum.com", + "8news.com.tw", + "8world.com", + "8z1.net", + "9001700.com", + "908taiwan.org", + "91jinman.com", + "91porn.com", + "91porny.com", + "91vps.club", + "92ccav.com", + "991.com", + "99btgc01.com", + "99cn.info", + "9bis.com", + "9bis.net", + "9cache.com", + "9gag.com", + "9news.com.au", + "a-normal-day.com", + "a5.com.ru", + "a5vpn.com", + "aamacau.com", + "abc.com", + "abc.net.au", + "abc.xyz", + "abchinese.com", + "abclite.net", + "abebooks.co.uk", + "abebooks.com", + "ablwang.com", + "aboluowang.com", + "about.google", + "about.me", + "aboutgfw.com", + "abs.edu", + "ac.jp", + "acast.com", + "accim.org", + "accountkit.com", + "aceros-de-hispania.com", + "acevpn.com", + "acg.rip", + "acg18.me", + "acgbox.link", + "acgbox.org", + "acgkj.com", + "acgnx.se", + "acmedia365.com", + "acmetoy.com", + "acnw.com.au", + "actfortibet.org", + "actimes.com.au", + "activpn.com", + "aculo.us", + "adcex.com", + "addictedtocoffee.de", + "addyoutube.com", + "adelaidebbs.com", + "adguard-vpn.com", + "admob.com", + "adpl.org.hk", + "ads-twitter.com", + "adsense.com", + "adult-sex-games.com", + "adultfriendfinder.com", + "adultkeep.net", + "advanscene.com", + "advertfan.com", + "advertisercommunity.com", + "ae.org", + "aei.org", + "aenhancers.com", + "aex.com", + "af.mil", + "afantibbs.com", + "afr.com", + "afreecatv.com", + "agemys.net", + "agnesb.fr", + "agoogleaday.com", + "agro.hk", + "ai-kan.net", + "ai-wen.net", + "ai.google", + "aiosearch.com", + "aiph.net", + "airasia.com", + "airconsole.com", + "aircrack-ng.org", + "airitilibrary.com", + "airvpn.org", + "aisex.com", + "ait.org.tw", + "aiv-cdn.net", + "aiv-delivery.net", + "aiweiwei.com", + "aiweiweiblog.com", + "ajsands.com", + "akademiye.org", + "akamai.net", + "akamaihd.net", + "akamaistream.net", + "akamaized.net", + "akiba-online.com", + "akiba-web.com", + "akinator.com", + "akow.org", + "al-islam.com", + "al-qimmah.net", + "alabout.com", + "alanhou.com", + "alarab.qa", + "alasbarricadas.org", + "alexlur.org", + "alforattv.net", + "alhayat.com", + "alicejapan.co.jp", + "aliengu.com", + "alive.bar", + "aljazeera.com", + "aljazeera.net", + "alkasir.com", + "all4mom.org", + "allcoin.com", + "allconnected.co", + "alldrawnsex.com", + "allervpn.com", + "allfinegirls.com", + "allgirlmassage.com", + "allgirlsallowed.org", + "allgravure.com", + "alliance.org.hk", + "allinfa.com", + "alljackpotscasino.com", + "allmovie.com", + "allowed.org", + "almasdarnews.com", + "almostmy.com", + "alphaporno.com", + "alternate-tools.com", + "alternativeto.net", + "altrec.com", + "alvinalexander.com", + "alwaysdata.com", + "alwaysdata.net", + "alwaysvpn.com", + "am730.com.hk", + "amazon.co.jp", + "amazon.com", + "amazonvideo.com", + "ameba.jp", + "ameblo.jp", + "america.gov", + "american.edu", + "americangreencard.com", + "americanunfinished.com", + "americorps.gov", + "amiblockedornot.com", + "amigobbs.net", + "amitabhafoundation.us", + "amnesty.org", + "amnesty.org.hk", + "amnesty.tw", + "amnestyusa.org", + "amnyemachen.org", + "amoiist.com", + "ampproject.org", + "amtb-taipei.org", + "anchor.fm", + "anchorfree.com", + "ancsconf.org", + "andfaraway.net", + "android-x86.org", + "android.com", + "androidapksfree.com", + "androidcombo.com", + "androidify.com", + "androidplus.co", + "androidtv.com", + "andygod.com", + "angela-merkel.de", + "angelfire.com", + "angola.org", + "angularjs.org", + "animecrazy.net", + "animeshippuuden.com", + "animezilla.com", + "aniscartujo.com", + "annas-archive.org", + "annas-archive.se", + "annatam.com", + "anobii.com", + "anonfiles.com", + "anontext.com", + "anonymise.us", + "anonymitynetwork.com", + "anonymizer.com", + "anonymouse.org", + "anpopo.com", + "answering-islam.org", + "antd.org", + "anthonycalzadilla.com", + "anthropic.com", + "anti1984.com", + "antichristendom.com", + "antisocial.science", + "antiwave.net", + "antpool.com", + "anws.gov.tw", + "anyporn.com", + "anysex.com", + "ao3.org", + "aobo.com.au", + "aofriend.com", + "aofriend.com.au", + "aojiao.org", + "aol.ca", + "aol.co.uk", + "aol.com", + "aolnews.com", + "aomiwang.com", + "ap.org", + "apartmentratings.com", + "apartments.com", + "apat1989.org", + "apetube.com", + "api.ai", + "apiary.io", + "apigee.com", + "apk-dl.com", + "apk.support", + "apk.tw", + "apkcombo.com", + "apkmirror.com", + "apkmonk.com", + "apkplz.com", + "apkpure.com", + "apkpure.net", + "aplusvpn.com", + "appadvice.com", + "appbrain.com", + "appdownloader.net", + "appledaily.com", + "appledaily.com.hk", + "appledaily.com.tw", + "appshopper.com", + "appsocks.net", + "appspot-preview.com", + "appspot.com", + "appsto.re", + "aptoide.com", + "archive.fo", + "archive.is", + "archive.li", + "archive.md", + "archive.org", + "archive.ph", + "archive.today", + "archive.vn", + "archiveofourown.com", + "archiveofourown.org", + "archives.gov", + "archives.gov.tw", + "arctosia.com", + "areca-backup.org", + "arena.taipei", + "arethusa.su", + "arlingtoncemetery.mil", + "army.mil", + "art4tibet1998.org", + "arte.tv", + "artofpeacefoundation.org", + "artstation.com", + "artsy.net", + "arunachalforests.gov.in", + "asacp.org", + "asdfg.jp", + "asg.to", + "asia-gaming.com", + "asiaharvest.org", + "asianage.com", + "asianews.it", + "asianfreeforum.com", + "asiansexdiary.com", + "asianspiss.com", + "asianwomensfilm.de", + "asiaone.com", + "asiatgp.com", + "asiatimes.com", + "asiatoday.us", + "ask.com", + "askstudent.com", + "askynz.net", + "aspi.org.au", + "aspistrategist.org.au", + "asrockind.com", + "assembla.com", + "assimp.org", + "astrill.com", + "atc.org.au", + "atchinese.com", + "atgfw.org", + "athenaeizou.com", + "atlanta168.com", + "atlaspost.com", + "atnext.com", + "audacy.com", + "audionow.com", + "authorizeddns.net", + "authorizeddns.org", + "authorizeddns.us", + "autodraw.com", + "av-e-body.com", + "av.com", + "av.movie", + "av01.tv", + "avaaz.org", + "avbody.tv", + "avcity.tv", + "avcool.com", + "avdb.in", + "avdb.tv", + "avfantasy.com", + "avg.com", + "avgle.com", + "avidemux.org", + "avmo.pw", + "avmoo.com", + "avmoo.net", + "avmoo.pw", + "avoision.com", + "avyahoo.com", + "axios.com", + "axureformac.com", + "azerbaycan.tv", + "azerimix.com", + "azirevpn.com", + "azubu.tv", + "azurewebsites.net", + "b-cdn.net", + "b-ok.cc", + "b0ne.com", + "baby-kingdom.com", + "babylonbee.com", + "babynet.com.hk", + "backchina.com", + "backpackers.com.tw", + "backtotiananmen.com", + "bad.news", + "badiucao.com", + "badjojo.com", + "badoo.com", + "bahamut.com.tw", + "baidu.jp", + "baijie.org", + "bailandaily.com", + "baixing.me", + "baizhi.org", + "bakgeekhome.tk", + "bamgrid.com", + "banana-vpn.com", + "band.us", + "bandcamp.com", + "bandpage.com", + "bandwagonhost.com", + "bangbrosnetwork.com", + "bangchen.net", + "bangdream.space", + "bangkokpost.com", + "bangumi.moe", + "bangyoulater.com", + "bankmobilevibe.com", + "bannedbook.org", + "bannednews.org", + "banorte.com", + "baramangaonline.com", + "barenakedislam.com", + "barnabu.co.uk", + "barton.de", + "bartvpn.com", + "bastillepost.com", + "bayvoice.net", + "baywords.com", + "bb-chat.tv", + "bbc.co.uk", + "bbc.com", + "bbc.in", + "bbcchinese.com", + "bbchat.tv", + "bbci.co.uk", + "bbg.gov", + "bbkz.com", + "bbnradio.org", + "bbs-tw.com", + "bbsdigest.com", + "bbsfeed.com", + "bbsland.com", + "bbsmo.com", + "bbsone.com", + "bbtoystore.com", + "bcast.co.nz", + "bcc.com.tw", + "bcchinese.net", + "bcex.ca", + "bcmorning.com", + "bcrncdn.com", + "bdsmvideos.net", + "beaconevents.com", + "beanfun.com", + "bearteach.com", + "bebo.com", + "beeg.com", + "beepool.com", + "beepool.org", + "beevpn.com", + "behance.net", + "behindkink.com", + "beijing1989.com", + "beijing2022.art", + "beijingspring.com", + "beijingzx.org", + "belamionline.com", + "bell.wiki", + "bemywife.cc", + "beric.me", + "berlinerbericht.de", + "berlintwitterwall.com", + "berm.co.nz", + "bestforchina.org", + "bestgore.com", + "bestpornstardb.com", + "bestvpn.com", + "bestvpnanalysis.com", + "bestvpnforchina.net", + "bestvpnserver.com", + "bestvpnservice.com", + "bestvpnusa.com", + "bet365.com", + "betaclouds.net", + "betfair.com", + "betternet.co", + "bettervpn.com", + "bettween.com", + "betvictor.com", + "bewww.net", + "beyondfirewall.com", + "bfnn.org", + "bfsh.hk", + "bgme.me", + "bgvpn.com", + "bianlei.com", + "biantailajiao.com", + "biantailajiao.in", + "biblesforamerica.org", + "bibliocommons.com", + "bibox.com", + "bic2011.org", + "bidswitch.net", + "biedian.me", + "big.one", + "bigfools.com", + "biggo.com.tw", + "bigjapanesesex.com", + "bigmoney.biz", + "bignews.org", + "bignewsnetwork.com", + "bigone.com", + "bigsound.org", + "bild.de", + "bilibili.tv", + "biliworld.com", + "billypan.com", + "binance.com", + "binance.us", + "binancezh.cc", + "bing.com", + "binux.me", + "binwang.me", + "bipic.net", + "bird.so", + "bit-z.com", + "bit.do", + "bit.ly", + "bitbay.net", + "bitchute.com", + "bitcointalk.org", + "bitcoinworld.com", + "bitfinex.com", + "bitget.com", + "bithumb.com", + "bitinka.com.ar", + "bitmex.com", + "bitshare.com", + "bitsnoop.com", + "bitterwinter.org", + "bitvise.com", + "bitz.ai", + "bitz.com", + "bizhat.com", + "bjnewlife.org", + "bjs.org", + "bjzc.org", + "bl-doujinsouko.com", + "blacked.com", + "blacklogic.com", + "blackvpn.com", + "blewpass.com", + "blingblingsquad.net", + "blinkx.com", + "blinw.com", + "blip.tv", + "blockcast.it", + "blockcn.com", + "blockedbyhk.com", + "blockless.com", + "blocktempo.com", + "blog.de", + "blog.google", + "blog.jp", + "blogblog.com", + "blogcatalog.com", + "blogcity.me", + "blogdns.org", + "blogger.com", + "blogimg.jp", + "blogjav.net", + "bloglines.com", + "bloglovin.com", + "blogs.com", + "blogspot.ae", + "blogspot.al", + "blogspot.am", + "blogspot.ba", + "blogspot.be", + "blogspot.bg", + "blogspot.ca", + "blogspot.cat", + "blogspot.ch", + "blogspot.cl", + "blogspot.co.uk", + "blogspot.com", + "blogspot.com.ar", + "blogspot.com.au", + "blogspot.com.br", + "blogspot.com.by", + "blogspot.com.co", + "blogspot.com.cy", + "blogspot.com.ee", + "blogspot.com.eg", + "blogspot.com.es", + "blogspot.com.mt", + "blogspot.com.ng", + "blogspot.com.tr", + "blogspot.com.uy", + "blogspot.cz", + "blogspot.de", + "blogspot.dk", + "blogspot.fi", + "blogspot.fr", + "blogspot.gr", + "blogspot.hk", + "blogspot.hr", + "blogspot.hu", + "blogspot.ie", + "blogspot.in", + "blogspot.is", + "blogspot.it", + "blogspot.jp", + "blogspot.kr", + "blogspot.li", + "blogspot.lt", + "blogspot.lu", + "blogspot.md", + "blogspot.mk", + "blogspot.mx", + "blogspot.my", + "blogspot.nl", + "blogspot.no", + "blogspot.pe", + "blogspot.pt", + "blogspot.qa", + "blogspot.ro", + "blogspot.ru", + "blogspot.se", + "blogspot.sg", + "blogspot.si", + "blogspot.sk", + "blogspot.sn", + "blogspot.tw", + "blogspot.ug", + "blogtd.net", + "blogtd.org", + "bloodshed.net", + "bloomberg.cn", + "bloomberg.com", + "bloomberg.de", + "bloombergview.com", + "bloomfortune.com", + "blubrry.com", + "blueangellive.com", + "bmdru.com", + "bmfinn.com", + "bnbstatic.com", + "bnews.co", + "bnext.com.tw", + "bnn.co", + "bnrmetal.com", + "bntrace.com", + "boardreader.com", + "bod.asia", + "bodog88.com", + "bolehvpn.net", + "bonbonme.com", + "bonbonsex.com", + "bonfoundation.org", + "bongacams.com", + "boobstagram.com", + "book.com.tw", + "bookdepository.com", + "bookepub.com", + "books.com.tw", + "booktopia.com.au", + "bookwalker.com.tw", + "boomssr.com", + "bootstrapcdn.com", + "borgenmagazine.com", + "bot.nu", + "botanwang.com", + "bowenpress.com", + "box.com", + "box.net", + "boxpn.com", + "boxun.com", + "boxun.tv", + "boxunblog.com", + "boxunclub.com", + "boyangu.com", + "boyfriendtv.com", + "boysfood.com", + "boysmaster.com", + "br.st", + "brainyquote.com", + "brandonhutchinson.com", + "braumeister.org", + "brave.com", + "bravotube.net", + "brazzers.com", + "breached.to", + "break.com", + "breakgfw.com", + "breaking911.com", + "breakingtweets.com", + "breakwall.net", + "briefdream.com", + "briian.com", + "brill.com", + "brizzly.com", + "brkmd.com", + "broadbook.com", + "broadpressinc.com", + "brockbbs.com", + "brookings.edu", + "brucewang.net", + "brutaltgp.com", + "bsky.app", + "bsky.network", + "bsky.social", + "bt2mag.com", + "bt95.com", + "btaia.com", + "btbit.net", + "btbtav.com", + "btbtt.co", + "btbtt.me", + "btc.com", + "btc98.com", + "btcbank.bank", + "btctrade.im", + "btdig.com", + "btdigg.org", + "btguard.com", + "btku.me", + "btku.org", + "btspread.com", + "btsynckeys.com", + "budaedu.org", + "buddhanet.com.tw", + "buddhistchannel.tv", + "buffered.com", + "bullguard.com", + "bullog.org", + "bullogger.com", + "bumingbai.net", + "bunbunhk.com", + "busayari.com", + "business-humanrights.org", + "business.page", + "business.site", + "businessinsider.com", + "businessinsider.com.au", + "businesstoday.com.tw", + "businessweek.com", + "businessweekly.com.tw", + "busu.org", + "busytrade.com", + "buugaa.com", + "buzzhand.com", + "buzzhand.net", + "buzzorange.com", + "buzzsprout.com", + "bvpn.com", + "bwbx.io", + "bwgyhw.com", + "bwh1.net", + "bwsj.hk", + "bx.in.th", + "bx.tl", + "bybit.com", + "bynet.co.il", + "bypasscensorship.org", + "byrut.org", + "byteoversea.com", + "c-est-simple.com", + "c-span.org", + "c-spanvideo.org", + "c100tibet.org", + "c2cx.com", + "c3pool.com", + "ca.gov", + "cableav.tv", + "cablegatesearch.net", + "cachefly.com", + "cachefly.net", + "cachinese.com", + "cacnw.com", + "cactusvpn.com", + "cafepress.com", + "cahr.org.tw", + "caijinglengyan.com", + "calameo.com", + "calebelston.com", + "calendarz.com", + "calgarychinese.ca", + "calgarychinese.com", + "calgarychinese.net", + "calibre-ebook.com", + "caltech.edu", + "cam4.com", + "cam4.jp", + "cam4.sg", + "camfrog.com", + "campaign-archive.com", + "campaignforuyghurs.org", + "cams.com", + "cams.org.sg", + "canadameet.com", + "canalporno.com", + "cantonese.asia", + "canyu.org", + "cao.im", + "caobian.info", + "caochangqing.com", + "caoporn.us", + "cap.org.hk", + "captainfawcett.com", + "carabinasypistolas.com", + "cardinalkungfoundation.org", + "careerengine.us", + "carfax.com", + "cari.com.my", + "caribbeancom.com", + "carmotorshow.com", + "carousell.com.hk", + "carrd.co", + "carryzhou.com", + "cartoon18.com", + "cartoonmovement.com", + "casadeltibetbcn.org", + "casatibet.org.mx", + "casinobellini.com", + "casinoking.com", + "casinoriva.com", + "castbox.fm", + "catbox.moe", + "catch22.net", + "catchgod.com", + "catfightpayperview.xxx", + "catholic.org.hk", + "catholic.org.tw", + "cathvoice.org.tw", + "cato.org", + "cattt.com", + "caus.com", + "cbc.ca", + "cbsnews.com", + "cbtc.org.hk", + "cc.com", + "cccat.cc", + "cccat.co", + "ccdtr.org", + "ccfd.org.tw", + "cchere.com", + "ccim.org", + "cclife.ca", + "cclife.org", + "cclifefl.org", + "ccthere.com", + "ccthere.net", + "cctmweb.net", + "cctongbao.com", + "ccu.edu.tw", + "ccue.ca", + "ccue.com", + "ccvoice.ca", + "ccw.org.tw", + "cdbook.org", + "cdcparty.com", + "cdef.org", + "cdig.info", + "cdjp.org", + "cdn-apple.com", + "cdn-telegram.org", + "cdnews.com.tw", + "cdninstagram.com", + "cdp1989.org", + "cdp1998.org", + "cdp2006.org", + "cdpa.url.tw", + "cdpeu.org", + "cdpuk.co.uk", + "cdpusa.org", + "cdpweb.org", + "cdpwu.org", + "cdw.com", + "cecc.gov", + "celestiallight.org", + "cellulo.info", + "cenews.eu", + "centauro.com.br", + "centerforhumanreprod.com", + "centralnation.com", + "centurys.net", + "certificate-transparency.org", + "cex.io", + "cfhks.org.hk", + "cfos.de", + "cfr.org", + "cfsh99.com", + "cftfc.com", + "cgdepot.org", + "cgst.edu", + "change.org", + "changeip.name", + "changeip.net", + "changeip.org", + "changp.com", + "changsa.net", + "channel8news.sg", + "channelnewsasia.com", + "chanworld.org", + "chaoex.com", + "chaos.social", + "chapm25.com", + "character.ai", + "chatgpt.com", + "chatnook.com", + "chaturbate.com", + "checkgfw.com", + "chengmingmag.com", + "chenguangcheng.com", + "chenpokong.com", + "chenpokong.net", + "chenpokongvip.com", + "cherrysave.com", + "chhongbi.org", + "chicagoncmtv.com", + "china-mmm.jp.net", + "china-mmm.net", + "china-mmm.sa.com", + "china-review.com.ua", + "china-week.com", + "china101.com", + "china18.org", + "china21.com", + "china21.org", + "china5000.us", + "chinaaffairs.org", + "chinaaid.me", + "chinaaid.net", + "chinaaid.org", + "chinaaid.us", + "chinachange.org", + "chinachannel.hk", + "chinacitynews.be", + "chinacomments.org", + "chinademocrats.org", + "chinadialogue.net", + "chinadigitaltimes.net", + "chinaelections.org", + "chinaeweekly.com", + "chinafile.com", + "chinafreepress.org", + "chinagate.com", + "chinageeks.org", + "chinagfw.org", + "chinagonet.com", + "chinagreenparty.org", + "chinahorizon.org", + "chinahrc.org", + "chinahush.com", + "chinainperspective.com", + "chinainterimgov.org", + "chinalaborwatch.org", + "chinalawandpolicy.com", + "chinalawtranslate.com", + "chinamule.com", + "chinamz.org", + "chinanewscenter.com", + "chinapost.com.tw", + "chinapress.com.my", + "chinarightsia.org", + "chinasmile.net", + "chinasocialdemocraticparty.com", + "chinasoul.org", + "chinasucks.net", + "chinatopsex.com", + "chinatown.com.au", + "chinatweeps.com", + "chinauncensored.tv", + "chinaway.org", + "chinaworker.info", + "chinaxchina.com", + "chinayouth.org.hk", + "chinayuanmin.org", + "chinese-hermit.net", + "chinese-leaders.org", + "chinese-memorial.org", + "chinesedaily.com", + "chinesedailynews.com", + "chinesedemocracy.com", + "chinesegay.org", + "chinesen.de", + "chinesenews.net.au", + "chinesepen.org", + "chineseradioseattle.com", + "chinesetalks.net", + "chineseupress.com", + "chingcheong.com", + "chinman.net", + "chithu.org", + "chobit.cc", + "chosun.com", + "chped.com", + "chrdnet.com", + "christianfreedom.org", + "christianstudy.com", + "christiantimes.org.hk", + "christusrex.org", + "chrlawyers.hk", + "chrome.com", + "chromecast.com", + "chromeenterprise.google", + "chromeexperiments.com", + "chromercise.com", + "chromestatus.com", + "chromium.org", + "cht.com.tw", + "chuang-yen.org", + "chubold.com", + "chubun.com", + "chuizi.net", + "churchinhongkong.org", + "chushigangdrug.ch", + "ci-en.jp", + "cia.gov", + "ciciai.com", + "cienen.com", + "cineastentreff.de", + "cipfg.org", + "circlethebayfortibet.org", + "cirosantilli.com", + "citizencn.com", + "citizenlab.ca", + "citizenlab.org", + "citizenpowerforchina.org", + "citizenscommission.hk", + "citizensradio.org", + "city365.ca", + "city9x.com", + "citypopulation.de", + "citytalk.tw", + "civicparty.hk", + "civildisobediencemovement.org", + "civilhrfront.org", + "civiliangunner.com", + "civilmedia.tw", + "civisec.org", + "civitai.com", + "cixiaoya.club", + "cjb.net", + "ck101.com", + "clarionproject.org", + "classicalguitarblog.net", + "claude.ai", + "clb.org.hk", + "cleansite.biz", + "cleansite.info", + "cleansite.us", + "clearharmony.net", + "clearsurance.com", + "clearwisdom.net", + "clementine-player.org", + "clickme.net", + "clinica-tibet.ru", + "clipconverter.cc", + "clipfish.de", + "clips4sale.com", + "cloakpoint.com", + "cloudcone.com", + "cloudflare-ipfs.com", + "cloudfront.net", + "cloudfunctions.net", + "cloudokyo.cloud", + "club1069.com", + "clubhouseapi.com", + "clyp.it", + "cmcn.org", + "cmegroup.com", + "cmi.org.tw", + "cmoinc.org", + "cms.gov", + "cmu.edu", + "cmule.com", + "cmule.org", + "cmx.im", + "cn-proxy.com", + "cn6.eu", + "cna.com.tw", + "cnabc.com", + "cnbeta.com.tw", + "cnd.org", + "cnet.com", + "cnex.org.cn", + "cnineu.com", + "cnitter.com", + "cnn.com", + "cnpolitics.org", + "cnproxy.com", + "cnyes.com", + "co.tv", + "coat.co.jp", + "cobinhood.com", + "cochina.co", + "cochina.org", + "code1984.com", + "codeplex.com", + "codeshare.io", + "codeskulptor.org", + "cofacts.tw", + "coin2co.in", + "coinbase.com", + "coinbene.com", + "coinegg.com", + "coinex.com", + "coingecko.com", + "coingi.com", + "coinmarketcap.com", + "coinrail.co.kr", + "cointiger.com", + "cointobe.com", + "coinut.com", + "colacloud.net", + "collateralmurder.com", + "collateralmurder.org", + "com.google", + "com.uk", + "comedycentral.com", + "comefromchina.com", + "comic-mega.me", + "comico.tw", + "commandarms.com", + "comments.app", + "commentshk.com", + "communistcrimes.org", + "communitychoicecu.com", + "comparitech.com", + "compileheart.com", + "compress.to", + "compython.net", + "congyu.moe", + "conoha.jp", + "constitutionalism.solutions", + "contactmagazine.net", + "convio.net", + "coobay.com", + "cool18.com", + "coolaler.com", + "coolder.com", + "coolloud.org.tw", + "coolncute.com", + "coolstuffinc.com", + "corumcollege.com", + "cos-moe.com", + "cosplayjav.pl", + "costco.com", + "cotweet.com", + "counter.social", + "coursehero.com", + "coze.com", + "cpj.org", + "cq99.us", + "crackle.com", + "crazypool.org", + "crazys.cc", + "crazyshit.com", + "crbug.com", + "crchina.org", + "crd-net.org", + "creaders.net", + "creadersnet.com", + "creativelab5.com", + "crisisresponse.google", + "cristyli.com", + "crocotube.com", + "crossfire.co.kr", + "crossthewall.net", + "crossvpn.net", + "crosswall.org", + "croxyproxy.com", + "crrev.com", + "crucial.com", + "crunchyroll.com", + "cruxpool.com", + "crwdcntrl.net", + "crypto.com", + "cryptographyengineering.com", + "csdparty.com", + "csis.org", + "csmonitor.com", + "csuchen.de", + "csw.org.uk", + "ct.org.tw", + "ctao.org", + "ctfriend.net", + "ctitv.com.tw", + "ctowc.org", + "cts.com.tw", + "ctwant.com", + "cuhk.edu.hk", + "cuhkacs.org", + "cuihua.org", + "cuiweiping.net", + "culture.tw", + "cumlouder.com", + "cuntcrack.com", + "curvefish.com", + "cusp.hk", + "cusu.hk", + "cutout.pro", + "cutscenes.net", + "cw.com.tw", + "cwb.gov.tw", + "cyberctm.com", + "cyberghostvpn.com", + "cynscribe.com", + "cytode.us", + "cz.cc", + "d-fukyu.com", + "d.cash", + "d0z.net", + "d100.net", + "d2bay.com", + "d2pass.com", + "dabr.co.uk", + "dabr.eu", + "dabr.me", + "dabr.mobi", + "dadazim.com", + "dadi360.com", + "dafabet.com", + "dafagood.com", + "dafahao.com", + "dafoh.org", + "daftporn.com", + "dagelijksestandaard.nl", + "daidostup.ru", + "dailidaili.com", + "dailymail.co.uk", + "dailymotion.com", + "dailysabah.com", + "dailyview.tw", + "daiphapinfo.net", + "dajiyuan.com", + "dajiyuan.de", + "dajiyuan.eu", + "dalailama-archives.org", + "dalailama.com", + "dalailama.mn", + "dalailama.ru", + "dalailama80.org", + "dalailamacenter.org", + "dalailamafellows.org", + "dalailamafilm.com", + "dalailamafoundation.org", + "dalailamahindi.com", + "dalailamainaustralia.org", + "dalailamajapanese.com", + "dalailamaprotesters.info", + "dalailamaquotes.org", + "dalailamatrust.org", + "dalailamavisit.org.nz", + "dalailamaworld.com", + "dalianmeng.org", + "daliulian.org", + "danke4china.net", + "danwei.org", + "daolan.net", + "daozhongxing.org", + "darktech.org", + "darktoy.net", + "darpa.mil", + "darrenliuwei.com", + "dashlane.com", + "dastrassi.org", + "data-vocabulary.org", + "data.gov.tw", + "datalabour.com", + "daum.net", + "david-kilgour.com", + "dawangidc.com", + "daxa.cn", + "daylife.com", + "db.tt", + "dbgjd.com", + "dcard.tw", + "dcmilitary.com", + "ddc.com.tw", + "ddex.io", + "ddhw.info", + "ddns.info", + "ddns.me.uk", + "ddns.mobi", + "ddns.ms", + "ddns.name", + "ddns.net", + "ddns.us", + "de-sci.org", + "deadhouse.org", + "deadline.com", + "deaftone.com", + "debug.com", + "deck.ly", + "deck.new", + "decodet.co", + "deepai.org", + "deepdiscount.com", + "deepmind.com", + "deezer.com", + "definebabe.com", + "deja.com", + "delcamp.net", + "delicious.com", + "democrats.org", + "demosisto.hk", + "depositphotos.com", + "derekhsu.homeip.net", + "desc.se", + "design.google", + "desipro.de", + "dessci.com", + "destroy-china.jp", + "detroitnews.com", + "deutsche-welle.de", + "deviantart.com", + "deviantart.net", + "devio.us", + "devpn.com", + "devv.ai", + "dfas.mil", + "dfn.org", + "dharamsalanet.com", + "dharmakara.net", + "dhcp.biz", + "diaoyuislands.org", + "difangwenge.org", + "dify.ai", + "digg.com", + "digiland.tw", + "digisfera.com", + "digitalnomadsproject.org", + "diigo.com", + "dilber.se", + "dingchin.com.tw", + "dipity.com", + "directcreative.com", + "discoins.com", + "disconnect.me", + "discord.com", + "discord.gg", + "discord.media", + "discordapp.com", + "discordapp.net", + "discuss.com.hk", + "discuss4u.com", + "dish.com", + "disk.yandex", + "disney-plus.net", + "disneyplus.com", + "disp.cc", + "disqus.com", + "dit-inc.us", + "diyin.org", + "dizhidizhi.com", + "dizhuzhishang.com", + "djangosnippets.org", + "djorz.com", + "dl-laby.jp", + "dlive.tv", + "dlsite.com", + "dlsite.jp", + "dlyoutube.com", + "dm530.net", + "dma.mil", + "dmarcnetworks.com", + "dmc.nico", + "dmcdn.net", + "dmhy.org", + "dmm.co.jp", + "dmm.com", + "dns-dns.com", + "dns-stuff.com", + "dns.google", + "dns04.com", + "dns05.com", + "dns1.us", + "dns2.us", + "dns2go.com", + "dnscrypt.org", + "dnset.com", + "dnsrd.com", + "dnssec.net", + "dnvod.tv", + "doc.new", + "docker.com", + "docker.io", + "docs.new", + "doctorvoice.org", + "documentingreality.com", + "dogfartnetwork.com", + "dojin.com", + "dok-forum.net", + "dolc.de", + "dolf.org.hk", + "dollf.com", + "domain.club.tw", + "domain.glass", + "domains.google", + "domaintoday.com.au", + "donga.com", + "dongtaiwang.com", + "dongtaiwang.net", + "dongyangjing.com", + "donmai.us", + "dontfilter.us", + "dontmovetochina.com", + "doosho.com", + "doourbest.org", + "dorjeshugden.com", + "dotplane.com", + "dotsub.com", + "dotvpn.com", + "doub.io", + "doubibackup.com", + "doubiyunbackup.com", + "doublethinklab.org", + "doubmirror.cf", + "douchi.space", + "dougscripts.com", + "douhokanko.net", + "doujincafe.com", + "dowei.org", + "dowjones.com", + "dphk.org", + "dpool.top", + "dpp.org.tw", + "dpr.info", + "dragonex.io", + "dragonsprings.org", + "dreamamateurs.com", + "drepung.org", + "drgan.net", + "drmingxia.org", + "dropbooks.tv", + "dropbox.com", + "dropboxapi.com", + "dropboxusercontent.com", + "drsunacademy.com", + "drtuber.com", + "dscn.info", + "dsmtp.com", + "dssott.com", + "dstk.dk", + "dtdns.net", + "dtiblog.com", + "dtic.mil", + "dtwang.org", + "duanzhihu.com", + "dubox.com", + "duck.com", + "duckdns.org", + "duckduckgo.com", + "duckload.com", + "duckmylife.com", + "duga.jp", + "duihua.org", + "duihuahrjournal.org", + "dumb1.com", + "dunyabulteni.net", + "duoweitimes.com", + "duping.net", + "duplicati.com", + "dupola.com", + "dupola.net", + "dushi.ca", + "duyaoss.com", + "dvdpac.com", + "dvorak.org", + "dw-world.com", + "dw-world.de", + "dw.com", + "dw.de", + "dweb.link", + "dwheeler.com", + "dwnews.com", + "dwnews.net", + "dxiong.com", + "dynamic-dns.net", + "dynamicdns.biz", + "dynamicdns.co.uk", + "dynamicdns.me.uk", + "dynamicdns.org.uk", + "dynawebinc.com", + "dyndns-ip.com", + "dyndns-pics.com", + "dyndns.org", + "dyndns.pro", + "dynssl.com", + "dynu.com", + "dynu.net", + "dysfz.cc", + "dzze.com", + "e-classical.com.tw", + "e-gold.com", + "e-hentai.org", + "e-hentaidb.com", + "e-info.org.tw", + "e-traderland.net", + "e-zone.com.hk", + "e123.hk", + "earlytibet.com", + "earthcam.com", + "earthvpn.com", + "eastasiaforum.org", + "eastern-ark.com", + "easternlightning.org", + "eastturkestan.com", + "eastturkistan-gov.org", + "eastturkistan.net", + "eastturkistancc.org", + "eastturkistangovernmentinexile.us", + "easyca.ca", + "easypic.com", + "ebc.net.tw", + "ebony-beauty.com", + "ebookbrowse.com", + "ebookee.com", + "ebtcbank.com", + "ecfa.org.tw", + "echainhost.com", + "echofon.com", + "ecimg.tw", + "eckosia.org", + "ecministry.net", + "economist.com", + "ecstart.com", + "edgecastcdn.net", + "edgesuite.net", + "edicypages.com", + "edmontonchina.cn", + "edmontonservice.com", + "edns.biz", + "edoors.com", + "edubridge.com", + "edupro.org", + "edx-cdn.org", + "eesti.ee", + "eevpn.com", + "efcc.org.hk", + "effers.com", + "efksoft.com", + "efreenews.com", + "efukt.com", + "eic-av.com", + "eireinikotaerukai.com", + "eisbb.com", + "eksisozluk.com", + "elconfidencial.com", + "electionsmeter.com", + "elgoog.im", + "ellawine.org", + "elpais.com", + "eltondisney.com", + "emaga.com", + "emanna.com", + "embr.in", + "emilylau.org.hk", + "emory.edu", + "empfil.com", + "emule-ed2k.com", + "emulefans.com", + "emuparadise.me", + "enanyang.my", + "enca.com", + "encrypt.me", + "encyclopedia.com", + "enewstree.com", + "enfal.de", + "engadget.com", + "engagedaily.org", + "englishforeveryone.org", + "englishfromengland.co.uk", + "englishpen.org", + "enlighten.org.tw", + "entermap.com", + "entnt.com", + "environment.google", + "epa.gov.tw", + "epac.to", + "episcopalchurch.org", + "epochhk.com", + "epochtimes-bg.com", + "epochtimes-romania.com", + "epochtimes.co.il", + "epochtimes.co.kr", + "epochtimes.com", + "epochtimes.com.tw", + "epochtimes.cz", + "epochtimes.de", + "epochtimes.fr", + "epochtimes.ie", + "epochtimes.it", + "epochtimes.jp", + "epochtimes.ru", + "epochtimes.se", + "epochtimestr.com", + "epochweek.com", + "epochweekly.com", + "eporner.com", + "eprice.com.hk", + "equinenow.com", + "erabaru.net", + "eracom.com.tw", + "eraysoft.com.tr", + "erepublik.com", + "erights.net", + "eriversoft.com", + "erktv.com", + "ernestmandel.org", + "erodaizensyu.com", + "erodoujinlog.com", + "erodoujinworld.com", + "eromanga-kingdom.com", + "eromangadouzin.com", + "eromon.net", + "eroprofile.com", + "eroticsaloon.net", + "eslite.com", + "esmtp.biz", + "esu.im", + "esurance.com", + "etaa.org.au", + "etadult.com", + "etaiwannews.com", + "etherdelta.com", + "ethermine.org", + "etherscan.com", + "etherscan.io", + "etizer.org", + "etokki.com", + "etowns.net", + "etowns.org", + "etsy.com", + "ettoday.net", + "etvonline.hk", + "eu.org", + "eucasino.com", + "eulam.com", + "eurekavpt.com", + "eurodasp.com", + "euronews.com", + "europa.eu", + "everipedia.org", + "evozi.com", + "evschool.net", + "exam.gov.tw", + "exblog.co.jp", + "exblog.jp", + "exchristian.hk", + "excite.co.jp", + "exhentai.org", + "exmo.com", + "exmormon.org", + "expatshield.com", + "expecthim.com", + "expekt.com", + "experts-univers.com", + "exploader.net", + "expofutures.com", + "expressvpn.com", + "exrates.me", + "extmatrix.com", + "extrabux.com", + "extremetube.com", + "exx.com", + "ey.gov.tw", + "eyevio.jp", + "eyny.com", + "ezpc.tk", + "ezpeer.com", + "ezua.com", + "f-droid.org", + "f2pool.com", + "f8.com", + "fa.gov.tw", + "facebook.br", + "facebook.com", + "facebook.de", + "facebook.design", + "facebook.hu", + "facebook.in", + "facebook.net", + "facebook.nl", + "facebook.se", + "facebookmail.com", + "facebookquotes4u.com", + "faceless.me", + "facesofnyfw.com", + "facesoftibetanselfimmolators.info", + "factchecklab.org", + "factpedia.org", + "fail.hk", + "faith100.org", + "faithfuleye.com", + "faiththedog.info", + "fakku.net", + "fallenark.com", + "falsefire.com", + "falun-co.org", + "falun-ny.net", + "falunart.org", + "falunasia.info", + "falunau.org", + "falunaz.net", + "falundafa-dc.org", + "falundafa-florida.org", + "falundafa-nc.org", + "falundafa-pa.net", + "falundafa-sacramento.org", + "falundafa.org", + "falundafaindia.org", + "falundafamuseum.org", + "falungong.club", + "falungong.de", + "falungong.org.uk", + "falunhr.org", + "faluninfo.de", + "faluninfo.net", + "falunpilipinas.net", + "falunworld.net", + "familyfed.org", + "famunion.com", + "fan-qiang.com", + "fanbox.cc", + "fandom.com", + "fangbinxing.com", + "fangeming.com", + "fangeqiang.com", + "fanglizhi.info", + "fangmincn.org", + "fangong.org", + "fangongheike.com", + "fanhaodang.com", + "fanhaolou.com", + "fanqiang.network", + "fanqiang.tk", + "fanqiangdang.com", + "fanqiangdang.org", + "fanqianghou.com", + "fanqiangyakexi.net", + "fanqiangzhe.com", + "fanswong.com", + "fantv.hk", + "fanyue.info", + "fapdu.com", + "faproxy.com", + "faqserv.com", + "fartit.com", + "farwestchina.com", + "fast.com", + "fastestvpn.com", + "fastly.net", + "fastpic.ru", + "fastssh.com", + "faststone.org", + "fatakat-n.club", + "fatbtc.com", + "favotter.net", + "favstar.fm", + "fawanghuihui.org", + "faydao.com", + "faz.net", + "fb.com", + "fb.me", + "fb.watch", + "fbaddins.com", + "fbcdn.net", + "fbsbx.com", + "fbworkmail.com", + "fc2.com", + "fc2blog.net", + "fc2china.com", + "fc2cn.com", + "fc2web.com", + "fda.gov.tw", + "fdbox.com", + "fdc64.de", + "fdc64.jp", + "fdc64.org", + "fdc89.jp", + "feedburner.com", + "feeder.co", + "feedly.com", + "feedx.net", + "feelssh.com", + "feer.com", + "feifeiss.com", + "feitian-california.org", + "feitianacademy.org", + "feixiaohao.com", + "feministteacher.com", + "fengzhenghu.com", + "fengzhenghu.net", + "fevernet.com", + "ff.im", + "fffff.at", + "fflick.com", + "ffvpn.com", + "fgmtv.net", + "fgmtv.org", + "fhreports.net", + "figprayer.com", + "fileflyer.com", + "fileforum.com", + "files2me.com", + "fileserve.com", + "filesor.com", + "fillthesquare.org", + "filmingfortibet.org", + "filthdump.com", + "financetwitter.com", + "finchvpn.com", + "findbook.tw", + "findmespot.com", + "findyoutube.com", + "findyoutube.net", + "fingerdaily.com", + "finler.net", + "firearmsworld.net", + "firebaseio.com", + "firefox.com", + "fireofliberty.info", + "fireofliberty.org", + "firetweet.io", + "firstfivefollowers.com", + "firstory.me", + "firstpost.com", + "firstrade.com", + "fish.audio", + "fizzik.com", + "flagsonline.it", + "flecheinthepeche.fr", + "fleshbot.com", + "fleursdeslettres.com", + "flexpool.io", + "flgg.us", + "flgjustice.org", + "flickr.com", + "flickrhivemind.net", + "flickriver.com", + "fling.com", + "flipboard.com", + "flipkart.com", + "flitto.com", + "flnet.org", + "flog.tw", + "flowhongkong.net", + "flurry.com", + "flypool.org", + "flyvpn.com", + "flyzy2005.com", + "fmnnow.com", + "fnac.be", + "fnac.com", + "fochk.org", + "focustaiwan.tw", + "focusvpn.com", + "fofg-europe.net", + "fofg.org", + "fofldfradio.org", + "foolsmountain.com", + "fooooo.com", + "footprint.net", + "footwiball.com", + "forbes.com", + "foreignaffairs.com", + "foreignpolicy.com", + "form.new", + "forms.new", + "forum4hk.com", + "forums-free.com", + "fotile.me", + "fountmedia.io", + "fourthinternational.org", + "foxbusiness.com", + "foxdie.us", + "foxgay.com", + "foxsub.com", + "foxtang.com", + "fpmt-osel.org", + "fpmt.org", + "fpmt.tw", + "fpmtmexico.org", + "fqok.org", + "fqrouter.com", + "frank2019.me", + "franklc.com", + "freakshare.com", + "free-gate.org", + "free-hada-now.org", + "free-proxy.cz", + "free-ss.site", + "free-ssh.com", + "free.bg", + "free.com.tw", + "free.fr", + "free4u.com.ar", + "freealim.com", + "freebeacon.com", + "freebearblog.org", + "freebrowser.org", + "freechal.com", + "freechina.net", + "freechina.news", + "freechinaforum.org", + "freechinaweibo.com", + "freeddns.com", + "freeddns.org", + "freedl.org", + "freedomchina.info", + "freedomcollection.org", + "freedomhongkong.org", + "freedomhouse.org", + "freedomsherald.org", + "freeforums.org", + "freefq.com", + "freefuckvids.com", + "freegao.com", + "freehongkong.org", + "freeilhamtohti.org", + "freekazakhs.org", + "freekwonpyong.org", + "freelotto.com", + "freeman2.com", + "freemoren.com", + "freemorenews.com", + "freemuse.org", + "freenet-china.org", + "freenetproject.org", + "freenewscn.com", + "freeones.com", + "freeopenvpn.com", + "freeoz.org", + "freeproxylists.net", + "freerk.com", + "freess.org", + "freessh.us", + "freetcp.com", + "freetibet.net", + "freetibet.org", + "freetibetanheroes.org", + "freetls.fastly.net", + "freetribe.me", + "freeviewmovies.com", + "freevpn.me", + "freevpn.nl", + "freewallpaper4.me", + "freewebs.com", + "freewechat.com", + "freeweibo.com", + "freewww.biz", + "freewww.info", + "freexinwen.com", + "freeyellow.com", + "freeyoutubeproxy.net", + "freezhihu.org", + "friday.tw", + "frienddy.com", + "friendfeed-media.com", + "friendfeed.com", + "friendfinder.com", + "friends-of-tibet.org", + "friendsoftibet.org", + "fril.jp", + "fring.com", + "fringenetwork.com", + "from-pr.com", + "from-sd.com", + "fromchinatousa.net", + "frommel.net", + "frontlinedefenders.org", + "frootvpn.com", + "froth.zone", + "fscked.org", + "fsurf.com", + "ft.com", + "ftchinese.com", + "ftp1.biz", + "ftpserver.biz", + "ftv.com.tw", + "ftvnews.com.tw", + "ftx.com", + "fucd.com", + "fuchsia.dev", + "fuckcnnic.net", + "fuckgfw.org", + "fuckgfw233.org", + "fulione.com", + "fullerconsideration.com", + "fullservicegame.com", + "fulue.com", + "funami.tech", + "funf.tw", + "funkyimg.com", + "funp.com", + "fuq.com", + "furbo.org", + "furhhdl.org", + "furinkan.com", + "furl.net", + "futurechinaforum.org", + "futuremessage.org", + "fux.com", + "fuyin.net", + "fuyindiantai.org", + "fuyu.org.tw", + "fw.cm", + "fxcm-chinese.com", + "fxnetworks.com", + "fzh999.com", + "fzh999.net", + "fzlm.com", + "g-area.org", + "g-desktop.ru", + "g-queen.com", + "g.co", + "g0v.social", + "g6hentai.com", + "gab.com", + "gabocorp.com", + "gaeproxy.com", + "gaforum.org", + "gagaoolala.com", + "galaxymacau.com", + "galenwu.com", + "gallup.com", + "galstars.net", + "game735.com", + "gamebase.com.tw", + "gamejolt.com", + "gamer.com.tw", + "gamerp.jp", + "gamez.com.tw", + "gamousa.com", + "ganges.com", + "ganjing.com", + "ganjing.world", + "ganjingworld.com", + "gaoming.net", + "gaopi.net", + "gaozhisheng.net", + "gaozhisheng.org", + "gardennetworks.com", + "gardennetworks.org", + "gartlive.com", + "gate-project.com", + "gate.io", + "gatecoin.com", + "gather.com", + "gatherproxy.com", + "gati.org.tw", + "gaybubble.com", + "gaycn.net", + "gayhub.com", + "gaymap.cc", + "gaymenring.com", + "gaytube.com", + "gaywatch.com", + "gazotube.com", + "gcc.org.hk", + "gclooney.com", + "gclubs.com", + "gcmasia.com", + "gcpnews.com", + "gcr.io", + "gdaily.org", + "gdbt.net", + "gdzf.org", + "geek-art.net", + "geekerhome.com", + "geekheart.info", + "gekikame.com", + "gelbooru.com", + "gemini.com", + "generated.photos", + "genius.com", + "geocities.co.jp", + "geocities.com", + "geocities.jp", + "geph.io", + "gerefoundation.org", + "get.app", + "get.dev", + "get.how", + "get.page", + "getastrill.com", + "getchu.com", + "getcloak.com", + "getfoxyproxy.org", + "getfreedur.com", + "getgom.com", + "geti2p.net", + "getiton.com", + "getjetso.com", + "getlantern.org", + "getmalus.com", + "getmdl.io", + "getoutline.org", + "getsession.org", + "getsocialscope.com", + "getsync.com", + "gettr.com", + "gettrials.com", + "gettyimages.hk", + "getuploader.com", + "gfbv.de", + "gfgold.com.hk", + "gfnormal05at.com", + "gfsale.com", + "gfw.org.ua", + "gfw.press", + "gfw.report", + "gfwatch.org", + "ggjav.com", + "ggpht.com", + "ggssl.com", + "ghanely.me", + "ghidra-sre.org", + "ghostpath.com", + "ghut.org", + "giantessnight.com", + "gifree.com", + "giga-web.jp", + "gigacircle.com", + "giganews.com", + "gigporno.ru", + "girlbanker.com", + "git.io", + "gitbook.io", + "gitbooks.io", + "githack.com", + "github.blog", + "github.com", + "github.io", + "githubassets.com", + "githubcopilot.com", + "githubusercontent.com", + "gizlen.net", + "gjczz.com", + "glarity.app", + "glass8.eu", + "global.ssl.fastly.net", + "globaljihad.net", + "globalmediaoutreach.com", + "globalmuseumoncommunism.org", + "globalrescue.net", + "globaltm.org", + "globalvoices.org", + "globalvoicesonline.org", + "globalvpn.net", + "glock.com", + "gloryhole.com", + "glorystar.me", + "gluckman.com", + "glype.com", + "gmail.com", + "gmgard.com", + "gmhz.org", + "gmiddle.com", + "gmiddle.net", + "gmll.org", + "gmodules.com", + "gmx.net", + "gnci.org.hk", + "gnews.org", + "go-pki.com", + "go-to-zlibrary.se", + "go.com", + "go141.com", + "go5.dev", + "goagent.biz", + "goagentplus.com", + "goagle.de", + "gobet.cc", + "godaddy.com", + "godfootsteps.org", + "godns.work", + "godoc.org", + "godsdirectcontact.co.uk", + "godsdirectcontact.org", + "godsdirectcontact.org.tw", + "godsimmediatecontact.com", + "gofundme.com", + "gogle.de", + "gogole.com", + "gogotunnel.com", + "gohappy.com.tw", + "gokbayrak.com", + "golang.org", + "goldbet.com", + "goldbetsports.com", + "golden-ages.org", + "goldeneyevault.com", + "goldenfrog.com", + "goldjizz.com", + "goldstep.net", + "goldwave.com", + "gongm.in", + "gongmeng.info", + "gongminliliang.com", + "gongwt.com", + "goo.gl", + "goo.gle", + "goo.ne.jp", + "good.news", + "gooday.xyz", + "gooddns.info", + "goodhope.school", + "goodnewsnetwork.org", + "goodreaders.com", + "goodreads.com", + "goodtv.com.tw", + "goodtv.tv", + "goofind.com", + "googel.de", + "google-analytics.com", + "google-base.de", + "google.ad", + "google.ae", + "google.al", + "google.am", + "google.as", + "google.at", + "google.az", + "google.ba", + "google.be", + "google.bf", + "google.bg", + "google.bi", + "google.bj", + "google.bs", + "google.bt", + "google.by", + "google.ca", + "google.cat", + "google.cd", + "google.cf", + "google.cg", + "google.ch", + "google.ci", + "google.cl", + "google.cm", + "google.cn", + "google.co.ao", + "google.co.bw", + "google.co.ck", + "google.co.cr", + "google.co.id", + "google.co.il", + "google.co.in", + "google.co.jp", + "google.co.ke", + "google.co.kr", + "google.co.ls", + "google.co.ma", + "google.co.mz", + "google.co.nz", + "google.co.th", + "google.co.tz", + "google.co.ug", + "google.co.uk", + "google.co.uz", + "google.co.ve", + "google.co.vi", + "google.co.za", + "google.co.zm", + "google.co.zw", + "google.com", + "google.com.af", + "google.com.ag", + "google.com.ai", + "google.com.ar", + "google.com.au", + "google.com.bd", + "google.com.bh", + "google.com.bn", + "google.com.bo", + "google.com.br", + "google.com.bz", + "google.com.co", + "google.com.cu", + "google.com.cy", + "google.com.do", + "google.com.ec", + "google.com.eg", + "google.com.et", + "google.com.fj", + "google.com.gh", + "google.com.gi", + "google.com.gt", + "google.com.hk", + "google.com.jm", + "google.com.kh", + "google.com.kw", + "google.com.lb", + "google.com.ly", + "google.com.mm", + "google.com.mt", + "google.com.mx", + "google.com.my", + "google.com.na", + "google.com.nf", + "google.com.ng", + "google.com.ni", + "google.com.np", + "google.com.om", + "google.com.pa", + "google.com.pe", + "google.com.pg", + "google.com.ph", + "google.com.pk", + "google.com.pr", + "google.com.py", + "google.com.qa", + "google.com.sa", + "google.com.sb", + "google.com.sg", + "google.com.sl", + "google.com.sv", + "google.com.tj", + "google.com.tr", + "google.com.tw", + "google.com.ua", + "google.com.uy", + "google.com.vc", + "google.com.vn", + "google.cv", + "google.cz", + "google.de", + "google.dev", + "google.dj", + "google.dk", + "google.dm", + "google.dz", + "google.ee", + "google.es", + "google.fi", + "google.fm", + "google.fr", + "google.ga", + "google.ge", + "google.gg", + "google.gl", + "google.gm", + "google.gp", + "google.gr", + "google.gy", + "google.hn", + "google.hr", + "google.ht", + "google.hu", + "google.ie", + "google.im", + "google.iq", + "google.is", + "google.it", + "google.je", + "google.jo", + "google.kg", + "google.ki", + "google.kz", + "google.la", + "google.li", + "google.lk", + "google.lt", + "google.lu", + "google.lv", + "google.md", + "google.me", + "google.mg", + "google.mk", + "google.ml", + "google.mn", + "google.ms", + "google.mu", + "google.mv", + "google.mw", + "google.ne", + "google.nl", + "google.no", + "google.nr", + "google.nu", + "google.pl", + "google.pn", + "google.ps", + "google.pt", + "google.ro", + "google.rs", + "google.ru", + "google.rw", + "google.sc", + "google.se", + "google.sh", + "google.si", + "google.sk", + "google.sm", + "google.sn", + "google.so", + "google.sr", + "google.st", + "google.td", + "google.tg", + "google.tk", + "google.tl", + "google.tm", + "google.tn", + "google.to", + "google.tt", + "google.vg", + "google.vu", + "google.ws", + "googleanalytics.com", + "googleapis.com", + "googleapps.com", + "googlearth.com", + "googleartproject.com", + "googleblog.com", + "googlebot.com", + "googlechinawebmaster.com", + "googlecode.com", + "googlecommerce.com", + "googledomains.com", + "googledrive.com", + "googleearth.com", + "googlefiber.net", + "googlegroups.com", + "googlehosted.com", + "googleideas.com", + "googleinsidesearch.com", + "googlelabs.com", + "googlelocal.nl", + "googlemail.com", + "googlemaps.sv", + "googlemashups.com", + "googlepagecreator.com", + "googleplay.com", + "googleplus.com", + "googlescholar.com", + "googlesile.com", + "googlesource.com", + "googlesyndication.com", + "googleusercontent.com", + "googlevideo.com", + "googleweblight.com", + "googlezip.net", + "gopetition.com", + "goproxing.net", + "goreforum.com", + "goregrish.com", + "gospelherald.com", + "got-game.org", + "gotdns.ch", + "gotgeeks.com", + "gotrusted.com", + "gotw.ca", + "gov.taipei", + "gov.tw", + "gr8domain.biz", + "gr8name.biz", + "gradconnection.com", + "grammaly.com", + "grandtrial.org", + "grangorz.org", + "graph.org", + "graphis.ne.jp", + "graphql.org", + "gravatar.com", + "greasespot.net", + "greasyfork.org", + "great-firewall.com", + "great-roc.org", + "greatfire.org", + "greatfirewall.biz", + "greatfirewallofchina.net", + "greatfirewallofchina.org", + "greatroc.org", + "greatroc.tw", + "greatzhonghua.org", + "greenfieldbookstore.com.hk", + "greenparty.org.tw", + "greenpeace.com.tw", + "greenpeace.org", + "greenreadings.com", + "greenvpn.net", + "greenvpn.org", + "grindr.com", + "grok.com", + "grotty-monday.com", + "ground.news", + "grow.google", + "gs-discuss.com", + "gsearch.media", + "gstatic.com", + "gstf.org", + "gtricks.com", + "gts-vpn.com", + "gtv.org", + "gtv1.org", + "gu-chu-sum.org", + "guaguass.com", + "guaguass.org", + "guancha.org", + "guaneryu.com", + "guangming.com.my", + "guangnianvpn.com", + "guardster.com", + "guishan.org", + "gumroad.com", + "gun-world.net", + "gunsamerica.com", + "gunsandammo.com", + "guo.media", + "guruonline.hk", + "gutteruncensored.com", + "gvideo.de", + "gvlib.com", + "gvm.com.tw", + "gvt0.com", + "gvt1.com", + "gvt3.com", + "gwave.com", + "gwins.org", + "gwtproject.org", + "gyalwarinpoche.com", + "gyatsostudio.com", + "gzm.tv", + "gzone-anime.info", + "h-china.org", + "h-comic.com", + "h-moe.com", + "h1n1china.org", + "h528.com", + "h5dm.com", + "h5galgame.me", + "hacg.club", + "hacg.in", + "hacg.li", + "hacg.me", + "hacg.red", + "hacken.cc", + "hacker.org", + "hackmd.io", + "hackthatphone.net", + "hahlo.com", + "haijiao.com", + "haiwaikan.com", + "hakkatv.org.tw", + "halktv.com.tr", + "handcraftedsoftware.org", + "hanime.tv", + "hanime1.me", + "hanminzu.org", + "hanunyi.com", + "hao.news", + "hao123.com", + "hao123img.com", + "happy-vpn.com", + "haproxy.org", + "hardsextube.com", + "harunyahya.com", + "hasi.wang", + "hatena.ne.jp", + "hautelook.com", + "hautelookcdn.com", + "have8.com", + "hbg.com", + "hbo.com", + "hcaptcha.com", + "hclips.com", + "hdlt.me", + "hdsky.me", + "hdtvb.net", + "hdzog.com", + "he.net", + "heartyit.com", + "heavy-r.com", + "hec.su", + "hecaitou.net", + "hechaji.com", + "heeact.edu.tw", + "hegre-art.com", + "helixstudios.net", + "helloandroid.com", + "helloavgirls.com", + "helloqueer.com", + "helloss.pw", + "hellotxt.com", + "hellouk.org", + "helpeachpeople.com", + "helplinfen.com", + "helpster.de", + "helpuyghursnow.org", + "helpzhuling.org", + "henduohao.com", + "hentai.to", + "hentaipaw.com", + "hentaitube.tv", + "hentaivideoworld.com", + "heqinglian.net", + "here.com", + "heritage.org", + "heroku.com", + "herokuapp.com", + "heungkongdiscuss.com", + "hexieshe.com", + "hexieshe.xyz", + "hexxeh.net", + "heyuedi.com", + "heywire.com", + "heyzo.com", + "hgamefree.info", + "hgseav.com", + "hhdcb3office.org", + "hhthesakyatrizin.org", + "hi-on.org.tw", + "hiccears.com", + "hidden-advent.org", + "hide.me", + "hidecloud.com", + "hidein.net", + "hideipvpn.com", + "hideman.net", + "hideme.nl", + "hidemy.name", + "hidemyass.com", + "hidemycomp.com", + "higfw.com", + "highpeakspureearth.com", + "highrockmedia.com", + "hightail.com", + "hihiforum.com", + "hihistory.net", + "hiitch.com", + "hikinggfw.org", + "hilive.tv", + "himalaya.exchange", + "himalayan-foundation.org", + "himalayanglacier.com", + "himemix.com", + "himemix.net", + "hindustantimes.com", + "hinet.net", + "hitbtc.com", + "hitomi.la", + "hiveon.net", + "hiwifi.com", + "hizb-ut-tahrir.info", + "hizb-ut-tahrir.org", + "hizbuttahrir.org", + "hjclub.info", + "hjd.tw", + "hjd2048.com", + "hk-pub.com", + "hk01.com", + "hk32168.com", + "hkacg.com", + "hkacg.net", + "hkatvnews.com", + "hkbc.net", + "hkbf.org", + "hkbookcity.com", + "hkchronicles.com", + "hkchurch.org", + "hkci.org.hk", + "hkcmi.edu", + "hkcnews.com", + "hkcoc.com", + "hkctu.org.hk", + "hkdailynews.com.hk", + "hkday.net", + "hkdc.us", + "hkdf.org", + "hkej.com", + "hkepc.com", + "hket.com", + "hkfaa.com", + "hkfreezone.com", + "hkfront.org", + "hkgalden.com", + "hkgolden.com", + "hkgpao.com", + "hkgreenradio.org", + "hkheadline.com", + "hkhkhk.com", + "hkhrc.org.hk", + "hkhrm.org.hk", + "hkip.org.uk", + "hkja.org.hk", + "hkjc.com", + "hkjp.org", + "hklft.com", + "hklts.org.hk", + "hkmap.live", + "hkopentv.com", + "hkpeanut.com", + "hkptu.org", + "hkreadingcity.net", + "hkreporter.com", + "hku.hk", + "hkusu.net", + "hkvwet.com", + "hkwcc.org.hk", + "hkzone.org", + "hmoegirl.com", + "hmonghot.com", + "hmv.co.jp", + "hmvdigital.ca", + "hmvdigital.com", + "hnjhj.com", + "hnntube.com", + "ho5ho.com", + "hojemacau.com.mo", + "hola.com", + "hola.org", + "holymountaincn.com", + "holyspiritspeaks.org", + "home.saxo", + "homedepot.com", + "homeperversion.com", + "homeservershow.com", + "honeynet.org", + "hongkongfp.com", + "hongmeimei.com", + "hongzhi.li", + "honven.xyz", + "hootsuite.com", + "hoover.org", + "hoovers.com", + "hopedialogue.org", + "hopto.org", + "hornygamer.com", + "hornytrip.com", + "horrorporn.com", + "hostloc.com", + "hotair.com", + "hotav.tv", + "hotcoin.com", + "hotels.cn", + "hotfrog.com.tw", + "hotgoo.com", + "hotpornshow.com", + "hotpot.hk", + "hotshame.com", + "hotspotshield.com", + "hottg.com", + "hotvpn.com", + "hougaige.com", + "house.gov", + "howtoforge.com", + "hoxx.com", + "hoy.tv", + "hoyolab.com", + "hpa.gov.tw", + "hpjav.com", + "hqcdp.org", + "hqjapanesesex.com", + "hqmovies.com", + "hrcchina.org", + "hrcir.com", + "hrea.org", + "hrichina.org", + "hrntt.org", + "hrtsea.com", + "hrw.org", + "hrweb.org", + "hsex.men", + "hsjp.net", + "hsselite.com", + "hst.net.tw", + "hstern.net", + "hstt.net", + "ht.ly", + "htkou.net", + "htl.li", + "html5rocks.com", + "https443.net", + "https443.org", + "hua-yue.net", + "huaglad.com", + "huaibinmall.com", + "huanghuagang.org", + "huangyiyu.com", + "huaren.us", + "huaren4us.com", + "huashangnews.com", + "huasing.org", + "huaxia-news.com", + "huaxiabao.org", + "huaxin.ph", + "huayuworld.org", + "hudatoriq.web.id", + "hudson.org", + "huffingtonpost.com", + "huffpost.com", + "huggingface.co", + "hugoroy.eu", + "huhaitai.com", + "huhamhire.com", + "huhangfei.com", + "huigui.tw", + "huiyi.in", + "hulkshare.com", + "hulu.com", + "huluim.com", + "humanparty.me", + "humanrightsbriefing.org", + "humanrightspressawards.org", + "hung-ya.com", + "hungerstrikeforaids.org", + "huobi.co", + "huobi.com", + "huobi.me", + "huobi.pro", + "huobi.sc", + "huobi.vc", + "huobipool.com", + "huobipro.com", + "huping.net", + "hurgokbayrak.com", + "hurriyet.com.tr", + "hustler.com", + "hustlercash.com", + "hut2.ru", + "hutianyi.net", + "hutong9.net", + "huyandex.com", + "hwadzan.tw", + "hwayue.org.tw", + "hwinfo.com", + "hxwk.org", + "hxwq.org", + "hybrid-analysis.com", + "hyperrate.com", + "hypothes.is", + "hyread.com.tw", + "hysteria.network", + "i-cable.com", + "i-part.com.tw", + "i-scmp.com", + "i1.hk", + "i2p2.de", + "i2runner.com", + "i818hk.com", + "iam.soy", + "iamtopone.com", + "iask.bz", + "iask.ca", + "iav19.com", + "iavian.net", + "ibiblio.org", + "ibit.am", + "iblist.com", + "iblogserv-f.net", + "ibros.org", + "ibtimes.com", + "ibvpn.com", + "ibytedtos.com", + "icams.com", + "icedrive.net", + "icerocket.com", + "icij.org", + "icl-fi.org", + "icoco.com", + "iconfactory.net", + "iconpaper.org", + "icu-project.org", + "idaiwan.com", + "idemocracy.asia", + "identi.ca", + "idiomconnection.com", + "idlcoyote.com", + "idouga.com", + "idreamx.com", + "idsam.com", + "idv.tw", + "ieasy5.com", + "ied2k.net", + "ienergy1.com", + "iepl.us", + "ifanqiang.com", + "ifcss.org", + "ifjc.org", + "ifreewares.com", + "ift.tt", + "ig.com", + "igcd.net", + "igfw.net", + "igfw.tech", + "igmg.de", + "ignitedetroit.net", + "igoogle.ae", + "igoogle.am", + "igoogle.as", + "igoogle.at", + "igoogle.az", + "igoogle.ba", + "igoogle.be", + "igoogle.bg", + "igoogle.ca", + "igoogle.cd", + "igoogle.ci", + "igoogle.co.id", + "igoogle.co.jp", + "igoogle.co.kr", + "igoogle.co.ma", + "igoogle.co.uk", + "igoogle.com", + "igoogle.de", + "igoogle.dj", + "igoogle.dk", + "igoogle.es", + "igoogle.fi", + "igoogle.fm", + "igoogle.fr", + "igoogle.gg", + "igoogle.gl", + "igoogle.gr", + "igoogle.ie", + "igoogle.is", + "igoogle.it", + "igoogle.jo", + "igoogle.kz", + "igoogle.mn", + "igoogle.ms", + "igoogle.nl", + "igoogle.no", + "igoogle.nu", + "igoogle.ro", + "igoogle.ru", + "igoogle.rw", + "igoogle.sc", + "igoogle.sh", + "igoogle.sm", + "igoogle.sn", + "igoogle.tk", + "igoogle.tm", + "igoogle.to", + "igoogle.tt", + "igoogle.vu", + "igoogle.ws", + "igotmail.com.tw", + "igvita.com", + "ihakka.net", + "ihao.org", + "iicns.com", + "ikstar.com", + "ikwb.com", + "ilbe.com", + "ilhamtohtiinstitute.org", + "illawarramercury.com.au", + "illusionfactory.com", + "ilove80.be", + "ilovelongtoes.com", + "im.tv", + "im88.tw", + "imageab.com", + "imagefap.com", + "imageflea.com", + "imageglass.org", + "images-gaytube.com", + "imageshack.us", + "imagevenue.com", + "imagezilla.net", + "imago-images.com", + "imb.org", + "imdb.com", + "img.ly", + "imgasd.com", + "imgchili.net", + "imgmega.com", + "imgur.com", + "imkev.com", + "imlive.co", + "imlive.com", + "immigration.gov.tw", + "immoral.jp", + "impact.org.au", + "impp.mn", + "improd.works", + "in-disguise.com", + "in.com", + "in99.org", + "incapdns.net", + "incloak.com", + "incredibox.fr", + "independent.co.uk", + "indiablooms.com", + "indianarrative.com", + "indiandefensenews.in", + "indianexpress.com", + "indiatimes.com", + "indiatoday.in", + "indiemerch.com", + "indsr.org.tw", + "info-graf.fr", + "informer.com", + "ingress.com", + "inherit.live", + "initiativesforchina.org", + "inkbunny.net", + "inkui.com", + "inlang.com", + "inmediahk.net", + "innermongolia.org", + "inoreader.com", + "inote.tw", + "insecam.org", + "inside.com.tw", + "insidevoa.com", + "instagram.com", + "instanthq.com", + "institut-tibetain.org", + "interactivebrokers.com", + "internet.org", + "internetdefenseleague.org", + "internetfreedom.org", + "internetpopculture.com", + "inthenameofconfuciusmovie.com", + "invidio.us", + "inxian.com", + "iownyour.biz", + "iownyour.org", + "ipalter.com", + "ipdefenseforum.com", + "ipfire.org", + "ipfs.io", + "iphone4hongkong.com", + "iphonehacks.com", + "iphonetaiwan.org", + "iphonix.fr", + "ipicture.ru", + "ipjetable.net", + "ipobar.com", + "ipoock.com", + "iportal.me", + "ippotv.com", + "ipredator.se", + "iptv.com.tw", + "iptvbin.com", + "ipvanish.com", + "iqiyi.com", + "iredmail.org", + "irib.ir", + "ironpython.net", + "ironsocket.com", + "is-a-hunter.com", + "is.gd", + "isaacmao.com", + "isasecret.com", + "isgreat.org", + "ishr.ch", + "islahhaber.net", + "islam.org.hk", + "islamawareness.net", + "islamhouse.com", + "islamicity.com", + "islamicpluralism.org", + "islamtoday.net", + "ismaelan.com", + "ismalltits.com", + "ismprofessional.net", + "isohunt.com", + "israbox.com", + "issuu.com", + "istars.co.nz", + "istarshine.com", + "istef.info", + "istiqlalhewer.com", + "istockphoto.com", + "isunaffairs.com", + "isuntv.com", + "isupportuyghurs.org", + "itaboo.info", + "itaiwan.gov.tw", + "italiatibet.org", + "itasoftware.com", + "itch.io", + "itemdb.com", + "itemfix.com", + "ithome.com.tw", + "itsaol.com", + "itshidden.com", + "itsky.it", + "itweet.net", + "iu45.com", + "iuhrdf.org", + "iuksky.com", + "ivacy.com", + "iverycd.com", + "ivonblog.com", + "ivpn.net", + "iwara.tv", + "ixquick.com", + "ixxx.com", + "iyouport.com", + "iyouport.org", + "izaobao.us", + "izihost.org", + "izles.net", + "izlesem.org", + "j.mp", + "jable.tv", + "jackjia.com", + "jamaat.org", + "jamestown.org", + "jamyangnorbu.com", + "jan.ai", + "jandyx.com", + "janwongphoto.com", + "japan-whores.com", + "japanhdv.com", + "japantimes.co.jp", + "jav.com", + "jav101.com", + "jav321.com", + "jav68.tv", + "jav777.cc", + "javakiba.org", + "javbus.co", + "javbus.com", + "javdb.com", + "javfinder.ai", + "javfor.me", + "javfree.me", + "javhd.com", + "javhip.com", + "javhub.net", + "javhuge.com", + "javlibrary.com", + "javmobile.net", + "javmoo.com", + "javmoo.xyz", + "javseen.com", + "javtag.com", + "javzoo.com", + "javzz.com", + "jbtalks.cc", + "jbtalks.com", + "jbtalks.my", + "jcpenney.com", + "jdwsy.com", + "jeanyim.com", + "jetos.com", + "jex.com", + "jfqu36.club", + "jfqu37.xyz", + "jgoodies.com", + "jiangweiping.com", + "jiaoyou8.com", + "jichangtj.com", + "jiehua.cz", + "jiepang.com", + "jieshibaobao.com", + "jifangge.com", + "jigglegifs.com", + "jigong1024.com", + "jigsy.com", + "jihadology.net", + "jiji.com", + "jims.net", + "jinbushe.org", + "jingpin.org", + "jingsim.org", + "jinhai.de", + "jinpianwang.com", + "jinrizhiyi.news", + "jinroukong.com", + "jintian.net", + "jinx.com", + "jiruan.net", + "jitouch.com", + "jizzthis.com", + "jjgirls.com", + "jkb.cc", + "jkforum.net", + "jkub.com", + "jma.go.jp", + "jmcomic.me", + "jmscult.com", + "joachims.org", + "jobso.tv", + "joinbbs.net", + "joinclubhouse.com", + "joinmastodon.org", + "joinpeertube.org", + "joins.com", + "jornaldacidadeonline.com.br", + "jotform.com", + "journalchretien.net", + "journalofdemocracy.org", + "joymiihub.com", + "joyourself.com", + "jp1lib.org", + "jpopforum.net", + "jquery.com", + "jqueryui.com", + "jrf.org.tw", + "jsdelivr.net", + "jsfiddle.net", + "jshell.net", + "jtvnw.net", + "jubushoushen.com", + "judicial.gov.tw", + "juhuaren.com", + "jukujo-club.com", + "juliepost.com", + "juliereyc.com", + "junauza.com", + "june4commemoration.org", + "junefourth-20.net", + "jungleheart.com", + "junglobal.net", + "juoaa.com", + "justdied.com", + "justfreevpn.com", + "justhost.ru", + "justicefortenzin.org", + "justmysocks.net", + "justmysocks1.net", + "justmysockscn.com", + "justpaste.it", + "justtristan.com", + "juyuange.org", + "juziyue.com", + "jw.org", + "jwmusic.org", + "jwplayer.com", + "jyxf.net", + "k-doujin.net", + "ka-wai.com", + "kadokawa.co.jp", + "kagyu.org", + "kagyu.org.za", + "kagyumonlam.org", + "kagyunews.com.hk", + "kagyuoffice.org", + "kagyuoffice.org.tw", + "kaiyuan.de", + "kakao.com", + "kalachakralugano.org", + "kanald.com.tr", + "kangye.org", + "kankan.today", + "kannewyork.com", + "kanshifang.com", + "kantie.org", + "kanzhongguo.com", + "kanzhongguo.eu", + "kaotic.com", + "karayou.com", + "karkhung.com", + "karmapa-teachings.org", + "karmapa.org", + "kawaiikawaii.jp", + "kawase.com", + "kba-tx.org", + "kcoolonline.com", + "kebrum.com", + "kechara.com", + "keepandshare.com", + "keezmovies.com", + "kemono.party", + "kendatire.com", + "kendincos.net", + "kenengba.com", + "keontech.net", + "kepard.com", + "keso.cn", + "kex.com", + "keycdn.com", + "kfd.me", + "khabdha.org", + "khatrimaza.org", + "khmusic.com.tw", + "kichiku-doujinko.com", + "kik.com", + "killwall.com", + "kimy.com.tw", + "kindle4rss.com", + "kindleren.com", + "kingdomsalvation.org", + "kinghost.com", + "kingkong.com.tw", + "kingstone.com.tw", + "kink.com", + "kinmen.org.tw", + "kinmen.travel", + "kinokuniya.com", + "kir.jp", + "kissbbao.cn", + "kissjav.com", + "kiwi.kz", + "kk-whys.co.jp", + "kkbox.com", + "kknews.cc", + "klip.me", + "kmuh.org.tw", + "knowledgerush.com", + "knowyourmeme.com", + "ko-fi.com", + "kobo.com", + "kobobooks.com", + "kodingen.com", + "kompozer.net", + "konachan.com", + "kone.com", + "koolsolutions.com", + "koornk.com", + "koranmandarin.com", + "korea.net", + "korenan2.com", + "kpkuang.org", + "kqes.net", + "kraken.com", + "krtco.com.tw", + "ksdl.org", + "ksnews.com.tw", + "kspcoin.com", + "ktzhk.com", + "kuaichedao.co", + "kucoin.com", + "kui.name", + "kukuku.uk", + "kun.im", + "kurashsultan.com", + "kurtmunger.com", + "kusocity.com", + "kwcg.ca", + "kwok7.com", + "kwongwah.com.my", + "kxsw.life", + "kyofun.com", + "kyohk.net", + "kyoyue.com", + "kyzyhello.com", + "kzaobao.com", + "kzeng.info", + "la-forum.org", + "labiennale.org", + "ladbrokes.com", + "lagranepoca.com", + "lala.im", + "lalulalu.com", + "lama.com.tw", + "lamayeshe.com", + "lamenhu.com", + "lamnia.co.uk", + "lamrim.com", + "landofhope.tv", + "lanterncn.cn", + "lantosfoundation.org", + "laod.cn", + "laogai.org", + "laogairesearch.org", + "laomiu.com", + "laowang.vip", + "laoyang.info", + "laptoplockdown.com", + "laqingdan.net", + "larsgeorge.com", + "lastcombat.com", + "lastfm.es", + "latelinenews.com", + "latibet.org", + "lausan.hk", + "law.com", + "lbank.info", + "le-vpn.com", + "leafyvpn.net", + "lecloud.net", + "ledger.com", + "leeao.com.cn", + "lefora.com", + "left21.hk", + "legalporno.com", + "legra.ph", + "legsjapan.com", + "leirentv.ca", + "leisurecafe.ca", + "leisurepro.com", + "lematin.ch", + "lemonde.fr", + "lenwhite.com", + "leorockwell.com", + "lerosua.org", + "lers.google", + "lesoir.be", + "lester850.info", + "letou.com", + "letscorp.net", + "letsencrypt.org", + "levyhsu.com", + "lflink.com", + "lflinkup.com", + "lflinkup.net", + "lflinkup.org", + "lfpcontent.com", + "lhakar.org", + "lhasocialwork.org", + "li.taipei", + "liangyou.net", + "liangzhichuanmei.com", + "lianyue.net", + "liaowangxizang.net", + "liberal.org.hk", + "libertysculpturepark.com", + "libertytimes.com.tw", + "libraryinformationtechnology.com", + "libredd.it", + "lidecheng.com", + "lifemiles.com", + "lighten.org.tw", + "lighti.me", + "lightnovel.cn", + "lightyearvpn.com", + "lih.kg", + "lihkg.com", + "like.com", + "lilaoshibushinilaoshi.com", + "limelight.moe", + "limiao.net", + "line-apps.com", + "line-scdn.net", + "line.me", + "linglingfa.com", + "lingualeo.com", + "lingvodics.com", + "link-o-rama.com", + "linkedin.com", + "linkideo.com", + "linksalpha.com", + "linktr.ee", + "linkuswell.com", + "linpie.com", + "linux.org.hk", + "linuxtoy.org", + "lionsroar.com", + "lipuman.com", + "liquiditytp.com", + "liquidvpn.com", + "list-manage.com", + "listennotes.com", + "listentoyoutube.com", + "listorious.com", + "litenews.hk", + "lithium.com", + "liu-xiaobo.org", + "liudejun.com", + "liuhanyu.com", + "liujianshu.com", + "liuxiaobo.net", + "liuxiaotong.com", + "live.com", + "livecoin.net", + "livedoor.jp", + "liveleak.com", + "livemint.com", + "livestation.com", + "livestream.com", + "livevideo.com", + "livingonline.us", + "livingstream.com", + "liwangyang.com", + "lizhizhuangbi.com", + "lkcn.net", + "llss.me", + "lmsys.org", + "lncn.org", + "load.to", + "lobsangwangyal.com", + "localbitcoins.com", + "localdomain.ws", + "localpresshk.com", + "lockestek.com", + "logbot.net", + "logiqx.com", + "logmein.com", + "logos.com.hk", + "londonchinese.ca", + "longhair.hk", + "longmusic.com", + "longtermly.net", + "longtoes.com", + "lookpic.com", + "looktoronto.com", + "loongese.com", + "lorenzetti.com.br", + "lotsawahouse.org", + "lotuslight.org.hk", + "lotuslight.org.tw", + "loukky.com", + "loved.hk", + "lovetvshow.com", + "lpsg.com", + "lrfz.com", + "lrip.org", + "lsd.org.hk", + "lsforum.net", + "lsm.org", + "lsmchinese.org", + "lsmkorean.org", + "lsmradio.com", + "lsmwebcast.com", + "lsxszzg.com", + "ltn.com.tw", + "luckydesigner.space", + "luke54.com", + "luke54.org", + "lupm.org", + "lushstories.com", + "luxebc.com", + "lvhai.org", + "lvv2.com", + "ly.gov.tw", + "lyfhk.net", + "lzjscript.com", + "lzmtnews.org", + "m-sport.co.uk", + "m-team.cc", + "m.me", + "m8008.com", + "macgamestore.com", + "machbbs.com", + "macrovpn.com", + "macts.com.tw", + "mad-ar.ch", + "madewithcode.com", + "madonna-av.com", + "madou.club", + "madrau.com", + "madthumbs.com", + "magic-net.info", + "mahabodhi.org", + "mahjongsoul.com", + "maiio.net", + "mail-archive.com", + "mail.ru", + "mailchimp.com", + "maildns.xyz", + "maiplus.com", + "maizhong.org", + "makemymood.com", + "makkahnewspaper.com", + "malaysiakini.com", + "mamingzhe.com", + "manchukuo.net", + "manchustate.org", + "mandiant.com", + "mangabz.com", + "mangafox.com", + "mangafox.me", + "mangmang.run", + "manhuabika.com", + "manhuache.com", + "manhuagui.com", + "maniash.com", + "manicur4ik.ru", + "mansion.com", + "mansionpoker.com", + "manta.com", + "manyvoices.news", + "maplew.com", + "marc.info", + "marguerite.su", + "martau.com", + "martincartoons.com", + "martinoei.com", + "martsangkagyuofficial.org", + "maruta.be", + "marxist.com", + "marxist.net", + "marxists.org", + "mash.to", + "mashash.com", + "maskedip.com", + "mastodon.cloud", + "mastodon.host", + "mastodon.online", + "mastodon.social", + "mastodon.xyz", + "matainja.com", + "material.io", + "mathable.io", + "mathiew-badimon.com", + "matome-plus.com", + "matome-plus.net", + "matrix.org", + "matsushimakaede.com", + "matters.news", + "matters.town", + "mattwilcox.net", + "maturejp.com", + "maxing.jp", + "mayimayi.com", + "mcadforums.com", + "mcaf.ee", + "mcfog.com", + "mcreasite.com", + "md-t.org", + "me.com", + "me.me", + "meansys.com", + "media.org.hk", + "mediachinese.com", + "mediafire.com", + "mediafreakcity.com", + "mediawiki.org", + "medicalnewstoday.com", + "medium.com", + "mee6.xyz", + "meetav.com", + "meetup.com", + "mefeedia.com", + "meforum.org", + "mefound.com", + "mega.co.nz", + "mega.io", + "mega.nz", + "megalodon.jp", + "megaproxy.com", + "megarotic.com", + "megavideo.com", + "megurineluka.com", + "meirixiaochao.com", + "meizhong.blog", + "meizhong.report", + "melon365.com", + "meltoday.com", + "memehk.com", + "memes.tw", + "memorybbs.com", + "memri.org", + "memrijttm.org", + "mercari.com", + "mercari.jp", + "mercatox.com", + "mercdn.net", + "mercyprophet.org", + "mergersandinquisitions.com", + "mergersandinquisitions.org", + "meridian-trust.org", + "meripet.biz", + "meripet.com", + "merit-times.com.tw", + "merlinblog.xyz", + "meshrep.com", + "mesotw.com", + "messenger.com", + "meta.com", + "metacafe.com", + "metacubex.one", + "metafilter.com", + "metart.com", + "metarthunter.com", + "meteorshowersonline.com", + "metro.taipei", + "metrohk.com.hk", + "metrolife.ca", + "metroradio.com.hk", + "mewe.com", + "meyou.jp", + "meyul.com", + "mfxmedia.com", + "mgoon.com", + "mgstage.com", + "mh4u.org", + "mhradio.org", + "mi.com", + "michaelmarketl.com", + "microsoft.com", + "microvpn.com", + "middle-way.net", + "mihk.hk", + "mihr.com", + "mihua.org", + "mikanani.me", + "mikesoltys.com", + "mikocon.com", + "milph.net", + "milsurps.com", + "mimiai.net", + "mimivip.com", + "mimivv.com", + "mindrolling.org", + "mingdemedia.org", + "minghui-a.org", + "minghui-b.org", + "minghui-school.org", + "minghui.or.kr", + "minghui.org", + "mingjinglishi.com", + "mingjingnews.com", + "mingjingtimes.com", + "mingpao.com", + "mingpaocanada.com", + "mingpaomonthly.com", + "mingpaonews.com", + "mingpaony.com", + "mingpaosf.com", + "mingpaotor.com", + "mingpaovan.com", + "mingshengbao.com", + "minhhue.net", + "miniforum.org", + "miningpoolhub.com", + "ministrybooks.org", + "minzhuhua.net", + "minzhuzhanxian.com", + "minzhuzhongguo.org", + "miraheze.org", + "miroguide.com", + "mirror.xyz", + "mirrorbooks.com", + "mirrormedia.com.tw", + "mirrormedia.mg", + "missav.com", + "mist.vip", + "mit.edu", + "mitao.com.tw", + "mitbbs.com", + "mitbbsau.com", + "miuipolska.pl", + "mixero.com", + "mixi.jp", + "mixpod.com", + "mixx.com", + "mizzmona.com", + "mjib.gov.tw", + "mk5000.com", + "mlc.ai", + "mlcool.com", + "mlzs.work", + "mm-cg.com", + "mmaaxx.com", + "mmmca.com", + "mnewstv.com", + "mobatek.net", + "mobile01.com", + "mobileways.de", + "moby.to", + "mobypicture.com", + "mod.io", + "modernchinastudies.org", + "moeaic.gov.tw", + "moeerolibrary.com", + "moegirl.org", + "moeshare.cc", + "mofa.gov.tw", + "mofaxiehui.com", + "mofos.com", + "mog.com", + "mohu.club", + "mohu.ml", + "mohu.rocks", + "mojim.com", + "mol.gov.tw", + "molihua.org", + "momoshop.com.tw", + "monar.ch", + "mondex.org", + "money-link.com.tw", + "moneydj.com", + "moneyhome.biz", + "monica.im", + "monitorchina.org", + "monitorware.com", + "monlamit.org", + "monocloud.me", + "monster.com", + "moodyz.com", + "moomoo.com", + "moon.fm", + "moonbbs.com", + "moonbbs.info", + "moonbingo.com", + "mooo.com", + "moptt.tw", + "morbell.com", + "moresci.sale", + "morningsun.org", + "moroneta.com", + "mos.ru", + "mosucloud.site", + "motherless.com", + "motiyun.com", + "motor4ik.ru", + "mousebreaker.com", + "movements.org", + "moviefap.com", + "mozilla.org", + "moztw.org", + "mp3buscador.com", + "mp3ye.eu", + "mpettis.com", + "mpfinance.com", + "mpinews.com", + "mponline.hk", + "mqxd.org", + "mrbasic.com", + "mrbonus.com", + "mrface.com", + "mrslove.com", + "mrtweet.com", + "msa-it.org", + "msguancha.com", + "msha.gov", + "msn.com", + "msn.com.tw", + "mstdn.social", + "mswe1.org", + "mthruf.com", + "mtw.tl", + "mtzfile.pw", + "mubi.com", + "muchosucko.com", + "mullvad.net", + "multiply.com", + "multiproxy.org", + "multiupload.com", + "mummysgold.com", + "murmur.tw", + "muscdn.com", + "musicade.net", + "musical.ly", + "musixmatch.com", + "muslimvideo.com", + "muzi.com", + "muzi.net", + "muzu.tv", + "mvdis.gov.tw", + "mvg.jp", + "mx981.com", + "my-formosa.com", + "my-private-network.co.uk", + "my-proxy.com", + "my03.com", + "my903.com", + "myactimes.com", + "myanniu.com", + "myaudiocast.com", + "myav.com.tw", + "mybbs.us", + "mybet.com", + "myca168.com", + "mycanadanow.com", + "mychat.to", + "mychinamyhome.com", + "mychinanet.com", + "mychinanews.com", + "mychinese.news", + "mycnnews.com", + "mycould.com", + "mydad.info", + "mydati.com", + "myddns.com", + "myeasytv.com", + "myeclipseide.com", + "myforum.com.hk", + "myfreecams.com", + "myfreepaysite.com", + "myfreshnet.com", + "myftp.info", + "myftp.name", + "myip.com", + "myiphide.com", + "myiphider.com", + "myjs.tw", + "mykomica.org", + "mylftv.com", + "mymaji.com", + "mymediarom.com", + "mymoe.moe", + "mymom.info", + "mymusic.net.tw", + "mynetav.net", + "mynetav.org", + "mynumber.org", + "myparagliding.com", + "mypicasa.com", + "mypicture.info", + "mypikpak.com", + "mypop3.net", + "mypop3.org", + "mypopescu.com", + "myradio.hk", + "myreadingmanga.info", + "mysecondarydns.com", + "mysinablog.com", + "myspace.com", + "myspacecdn.com", + "mytalkbox.com", + "mytizi.com", + "mywife.cc", + "mywww.biz", + "myz.info", + "naacoalition.org", + "nabble.com", + "naitik.net", + "nakido.com", + "nakuz.com", + "nalandabodhi.org", + "nalandawest.org", + "namgyal.org", + "namgyalmonastery.org", + "namsisi.com", + "nanhuyt.com", + "nanopool.org", + "nanyang.com", + "nanyangpost.com", + "nanzao.com", + "naol.ca", + "naol.cc", + "narod.ru", + "nasa.gov", + "nat.gov.tw", + "nat.moe", + "natado.com", + "national-lottery.co.uk", + "nationalawakening.org", + "nationalgeographic.com", + "nationalinterest.org", + "nationalreview.com", + "nationsonline.org", + "nationwide.com", + "naughtyamerica.com", + "naver.jp", + "navy.mil", + "naweeklytimes.com", + "nbc.com", + "nbcnews.com", + "nbtvpn.com", + "nbyy.tv", + "nccwatch.org.tw", + "nch.com.tw", + "nchrd.org", + "ncn.org", + "ncol.com", + "nde.de", + "ndi.org", + "ndr.de", + "ndtv.com", + "ned.org", + "nekoslovakia.net", + "nengcard.com", + "neo-miracle.com", + "neowin.net", + "nepusoku.com", + "nesnode.com", + "net-fits.pro", + "netalert.me", + "netbig.com", + "netbirds.com", + "netcolony.com", + "netfirms.com", + "netflav.com", + "netflix.com", + "netflix.net", + "netlify.app", + "netme.cc", + "netsarang.com", + "netsneak.com", + "network54.com", + "networkedblogs.com", + "networktunnel.net", + "neverforget8964.org", + "new-3lunch.net", + "new-akiba.com", + "new96.ca", + "newcenturymc.com", + "newcenturynews.com", + "newchen.com", + "newgrounds.com", + "newhighlandvision.com", + "newipnow.com", + "newlandmagazine.com.au", + "newmitbbs.com", + "newnews.ca", + "news.com.au", + "news1.kr", + "news100.com.tw", + "news18.com", + "newsancai.com", + "newsblur.com", + "newschinacomment.org", + "newscn.org", + "newsdetox.ca", + "newsdh.com", + "newsmagazine.asia", + "newsmax.com", + "newspeak.cc", + "newstamago.com", + "newstapa.org", + "newstarnet.com", + "newstatesman.com", + "newsweek.com", + "newtaiwan.com.tw", + "newtalk.tw", + "newthuhole.com", + "newyorker.com", + "newyorktimes.com", + "nexon.com", + "next11.co.jp", + "nextapple.com", + "nextapple.tw", + "nextdigital.com.hk", + "nextmag.com.tw", + "nextmedia.com", + "nexton-net.jp", + "nexttv.com.tw", + "nf.id.au", + "nfjtyd.com", + "nflxext.com", + "nflximg.com", + "nflximg.net", + "nflxso.net", + "nflxvideo.net", + "ng.mil", + "nga.mil", + "ngensis.com", + "ngodupdongchung.com", + "nhentai.net", + "nhi.gov.tw", + "nhk-ondemand.jp", + "nic.google", + "nic.gov", + "nicovideo.jp", + "nighost.org", + "nightlife141.com", + "nightswatch.top", + "nike.com", + "nikkei.com", + "ninecommentaries.com", + "ning.com", + "ninjacloak.com", + "ninjaproxy.ninja", + "nintendium.com", + "ninth.biz", + "nitter.cc", + "nitter.net", + "niu.moe", + "niusnews.com", + "njactb.org", + "njav.tv", + "njuice.com", + "nlfreevpn.com", + "nmsl.website", + "nnews.eu", + "no-ip.com", + "no-ip.org", + "nobel.se", + "nobelprize.org", + "nobodycanstop.us", + "nodeseek.com", + "nodesnoop.com", + "nofile.io", + "nokogiri.org", + "nokola.com", + "noodlevpn.com", + "norbulingka.org", + "nordcdn.com", + "nordstrom.com", + "nordstromimage.com", + "nordstrommedia.com", + "nordstromrack.com", + "nordvpn.com", + "nos.nl", + "notepad-plus-plus.org", + "notion.site", + "nottinghampost.com", + "novelasia.com", + "now.com", + "now.im", + "nownews.com", + "nowtorrents.com", + "noxinfluencer.com", + "noypf.com", + "npa.go.jp", + "npa.gov.tw", + "npm.edu.tw", + "npm.gov.tw", + "npnt.me", + "npsboost.com", + "nradio.me", + "nrk.no", + "ns.ci", + "ns01.biz", + "ns01.info", + "ns01.us", + "ns02.biz", + "ns02.info", + "ns02.us", + "ns1.name", + "ns2.name", + "ns3.name", + "nsc.gov.tw", + "ntbk.gov.tw", + "ntbna.gov.tw", + "ntbt.gov.tw", + "ntd.tv", + "ntdsf.tv", + "ntdtv.ca", + "ntdtv.co.kr", + "ntdtv.com", + "ntdtv.com.tw", + "ntdtv.cz", + "ntdtv.jp", + "ntdtv.org", + "ntdtv.ru", + "ntdtvla.com", + "ntrfun.com", + "ntsna.gov.tw", + "ntu.edu.tw", + "nu.nl", + "nubiles.net", + "nudezz.com", + "nuexpo.com", + "nukistream.com", + "nurgo-software.com", + "nusatrip.com", + "nutaku.net", + "nutsvpn.work", + "nuuvem.com", + "nuvid.com", + "nuzcom.com", + "nvdst.com", + "nvquan.org", + "nvtongzhisheng.org", + "nwtca.org", + "nyaa.eu", + "nyaa.si", + "nybooks.com", + "nydailynews.com", + "nydus.ca", + "nyinfor.com", + "nylon-angel.com", + "nylonstockingsonline.com", + "nypost.com", + "nyt.com", + "nytchina.com", + "nytcn.me", + "nytco.com", + "nyti.ms", + "nytimes.com", + "nytimes.map.fastly.net", + "nytimg.com", + "nytlog.com", + "nytstyle.com", + "nzchinese.com", + "nzchinese.net.nz", + "o3o.ca", + "oaistatic.com", + "oaiusercontent.com", + "oanda.com", + "oann.com", + "oauth.net", + "observechina.net", + "obutu.com", + "obyte.org", + "ocaspro.com", + "occupytiananmen.com", + "oclp.hk", + "ocreampies.com", + "ocry.com", + "october-review.org", + "oculus.com", + "oculuscdn.com", + "odysee.com", + "oex.com", + "offbeatchina.com", + "officeoftibet.com", + "ofile.org", + "ogaoga.org", + "ogate.org", + "ohchr.org", + "ohmyrss.com", + "oikos.com.tw", + "oiktv.com", + "oizoblog.com", + "ok.ru", + "ok.xxx", + "okayfreedom.com", + "okcoin.com", + "okex.com", + "okk.tw", + "okpool.me", + "okx.com", + "olabloga.pl", + "old-cat.net", + "olehdtv.com", + "olelive.com", + "olevod.com", + "olumpo.com", + "olympicwatch.org", + "omct.org", + "omgili.com", + "omni7.jp", + "omnitalk.com", + "omnitalk.org", + "omny.fm", + "omtrdc.net", + "omy.sg", + "on.cc", + "on2.com", + "onapp.com", + "one.one", + "onedrive.com", + "onedumb.com", + "onejav.com", + "onesto.re", + "onestore.co.kr", + "onevps.com", + "onion.city", + "onion.ly", + "onion666.com", + "onlinecha.com", + "onlineyoutube.com", + "onlygayvideo.com", + "onlytweets.com", + "onmoon.com", + "onmoon.net", + "onmypc.biz", + "onmypc.info", + "onmypc.net", + "onmypc.org", + "onmypc.us", + "onthehunt.com", + "ontrac.com", + "oopsforum.com", + "open-assistant.io", + "open.com.hk", + "openai.com", + "openallweb.com", + "opendemocracy.net", + "opendn.xyz", + "openervpn.in", + "openid.net", + "openleaks.org", + "opensea.io", + "opensource.google", + "openstreetmap.org", + "opentech.fund", + "openvpn.net", + "openvpn.org", + "openwebster.com", + "openwrt.org.cn", + "opera-mini.net", + "opera.com", + "opus-gaming.com", + "oraclecloud.com", + "orchidbbs.com", + "organcare.org.tw", + "organharvestinvestigation.net", + "organiccrap.com", + "orgasm.com", + "orgfree.com", + "oricon.co.jp", + "orient-doll.com", + "orientaldaily.com.my", + "orn.jp", + "orzdream.com", + "orzistic.org", + "osfoora.com", + "otcbtc.com", + "otnd.org", + "otto.de", + "otzo.com", + "ourdearamy.com", + "ourhobby.com", + "oursogo.com", + "oursteps.com.au", + "oursweb.net", + "ourtv.hk", + "over-blog.com", + "overcast.fm", + "overdaily.org", + "overplay.net", + "ovi.com", + "ovpn.com", + "ow.ly", + "owind.com", + "owl.li", + "owltail.com", + "oxfordscholarship.com", + "oxid.it", + "oyax.com", + "oyghan.com", + "ozchinese.com", + "ozvoice.org", + "ozxw.com", + "ozyoyo.com", + "pachosting.com", + "pacificpoker.com", + "packetix.net", + "pacom.mil", + "pacopacomama.com", + "padmanet.com", + "page.link", + "page.tl", + "page2rss.com", + "pages.dev", + "pagodabox.com", + "paimon.moe", + "palacemoon.com", + "paldengyal.com", + "paljorpublications.com", + "palmislife.com", + "paltalk.com", + "pandafan.pub", + "pandapow.co", + "pandapow.net", + "pandavpn-jp.com", + "pandavpnpro.com", + "pandora.com", + "pandora.tv", + "panluan.net", + "panoramio.com", + "pao-pao.net", + "paper.li", + "paperb.us", + "paperclip.tk", + "paradisehill.cc", + "paradisepoker.com", + "parkansky.com", + "parler.com", + "parse.com", + "parsevideo.com", + "partycasino.com", + "partypoker.com", + "passion.com", + "passiontimes.hk", + "passwords.google", + "paste.ee", + "pastebin.com", + "pastie.org", + "pathtosharepoint.com", + "patreon.com", + "patreonusercontent.com", + "pawoo.net", + "paxful.com", + "pbs.org", + "pbwiki.com", + "pbworks.com", + "pbxes.com", + "pbxes.org", + "pcanywhere.net", + "pcc.gov.tw", + "pcdvd.com.tw", + "pcgamestorrents.com", + "pchome.com.tw", + "pchomeec.tw", + "pcij.org", + "pcloud.com", + "pcmarket.com.hk", + "pcstore.com.tw", + "pct.org.tw", + "pdetails.com", + "pdproxy.com", + "peace.ca", + "peacefire.org", + "peacehall.com", + "pearlher.org", + "peeasian.com", + "peing.net", + "pekingduck.org", + "pemulihan.or.id", + "pen.io", + "penchinese.com", + "penchinese.net", + "pendrivelinux.com", + "pentalogic.net", + "penthouse.com", + "pentoy.hk", + "peoplebookcafe.com", + "peoplenews.tw", + "peopo.org", + "percy.in", + "perfect-privacy.com", + "perfectgirls.net", + "perfectvpn.net", + "periscope.tv", + "perplexity.ai", + "persecutionblog.com", + "persiankitty.com", + "pfd.org.hk", + "phapluan.org", + "phayul.com", + "philborges.com", + "phmsociety.org", + "phncdn.com", + "phonegap.com", + "photodharma.net", + "photofocus.com", + "photonmedia.net", + "phprcdn.com", + "phptutorial.net", + "phuquocservices.com", + "piaotia.com", + "picacomic.com", + "picacomiccn.com", + "picasaweb.com", + "picidae.net", + "picturedip.com", + "picturesocial.com", + "picuki.com", + "pigav.com", + "pimg.tw", + "pin-cong.com", + "pin6.com", + "pincong.rocks", + "ping.fm", + "pinimg.com", + "pinkrod.com", + "pinoy-n.com", + "pinterest.at", + "pinterest.ca", + "pinterest.co.uk", + "pinterest.com", + "pinterest.de", + "pinterest.dk", + "pinterest.fr", + "pinterest.jp", + "pinterest.nl", + "pinterest.se", + "pionex.com", + "pipii.tv", + "piposay.com", + "piraattilahti.org", + "piring.com", + "pixeldrain.com", + "pixelqi.com", + "pixiv.net", + "pixiv.org", + "pixivsketch.net", + "pixnet.in", + "pixnet.net", + "pk.com", + "pki.goog", + "pkqjiasu.com", + "pkuanvil.com", + "placemix.com", + "play-asia.com", + "playbeasts.com", + "playboy.com", + "playboyplus.com", + "player.fm", + "playno1.com", + "playpcesor.com", + "plays.com.tw", + "plexvpn.pro", + "plixi.com", + "plm.org.hk", + "plunder.com", + "plurk.com", + "plus.codes", + "plus28.com", + "plusbb.com", + "pmatehunter.com", + "pmates.com", + "po2b.com", + "pobieramy.top", + "podbean.com", + "podcast.app", + "podcast.co", + "podictionary.com", + "poe.com", + "points-media.com", + "pokerstars.com", + "pokerstars.net", + "pokerstrategy.com", + "politicalchina.org", + "politicalconsultation.org", + "politico.eu", + "politiscales.net", + "poloniex.com", + "polymarket.com", + "polymer-project.org", + "polymerhk.com", + "poolbinance.com", + "poolin.com", + "popai.pro", + "popo.tw", + "popvote.hk", + "popxi.click", + "popyard.com", + "popyard.org", + "porn.biz", + "porn.com", + "porn2.com", + "porn5.com", + "pornbase.org", + "pornerbros.com", + "pornhd.com", + "pornhost.com", + "pornhub.com", + "pornhubdeutsch.net", + "pornhubpremium.com", + "pornmm.net", + "pornoxo.com", + "pornrapidshare.com", + "pornsharing.com", + "pornsocket.com", + "pornstarbyface.com", + "pornstarclub.com", + "porntube.com", + "porntubenews.com", + "porntvblog.com", + "pornvisit.com", + "port25.biz", + "portablevpn.nl", + "poskotanews.com", + "post01.com", + "post76.com", + "post852.com", + "postadult.com", + "postimg.org", + "potato.im", + "potatso.com", + "potvpn.com", + "pourquoi.tw", + "power.com", + "powerapple.com", + "powercx.com", + "powerphoto.org", + "powerpointninja.com", + "pp.ru", + "ppy.sh", + "prayforchina.net", + "premeforwindows7.com", + "premproxy.com", + "presentation.new", + "presentationzen.com", + "presidentlee.tw", + "prestige-av.com", + "prettyvirgin.com", + "pride.google", + "primevideo.com", + "printfriendly.com", + "prism-break.org", + "prisoneralert.com", + "pritunl.com", + "privacybox.de", + "private.com", + "privateinternetaccess.com", + "privatepaste.com", + "privatetunnel.com", + "privatevpn.com", + "privoxy.org", + "procopytips.com", + "prohashing.com", + "project-syndicate.org", + "prosiben.de", + "proton.me", + "protonvpn.com", + "provideocoalition.com", + "provpnaccounts.com", + "proxfree.com", + "proxifier.com", + "proxlet.com", + "proxomitron.info", + "proxpn.com", + "proxy.org", + "proxy1.xyz", + "proxyanonimo.es", + "proxydns.com", + "proxylist.org.uk", + "proxynetwork.org.uk", + "proxypy.net", + "proxyroad.com", + "proxytunnel.net", + "proxz.com", + "proyectoclubes.com", + "prozz.net", + "psblog.name", + "pscp.tv", + "pshvpn.com", + "psiphon.ca", + "psiphon3.com", + "psiphontoday.com", + "pstatic.net", + "pt.im", + "pts.org.tw", + "ptt.cc", + "pttgame.com", + "ptthito.com", + "pttvan.org", + "pttweb.cc", + "pttyes.com", + "ptwxz.com", + "pubu.com.tw", + "puffin.com", + "puffinbrowser.com", + "puffstore.com", + "pugpig.com", + "pullfolio.com", + "punjab.gov.in", + "punjab.gov.pk", + "punyu.com", + "pure18.com", + "pureapk.com", + "pureconcepts.net", + "pureinsight.org", + "purepdf.com", + "purevpn.com", + "purplelotus.org", + "pursuestar.com", + "pushchinawall.com", + "pussthecat.org", + "pussyspace.com", + "putihome.org", + "putlocker.com", + "putty.org", + "puuko.com", + "pwned.com", + "pximg.net", + "python.com", + "python.com.tw", + "pythonhackers.com", + "pythonic.life", + "pytorch.org", + "qanote.com", + "qbittorrent.org", + "qgirl.com.tw", + "qhigh.com", + "qi-gong.me", + "qianbai.tw", + "qiandao.today", + "qianglie.com", + "qiangwaikan.com", + "qiangyou.org", + "qianmo.tw", + "qidian.ca", + "qienkuen.org", + "qiwen.lu", + "qixianglu.cn", + "qkshare.com", + "qmzdd.com", + "qoos.com", + "qpoe.com", + "qq.co.za", + "qstatus.com", + "qtrac.eu", + "qtweeter.com", + "quannengshen.org", + "quantumbooter.net", + "questvisual.com", + "quitccp.net", + "quitccp.org", + "quiz.directory", + "quora.com", + "quoracdn.net", + "quran.com", + "quranexplorer.com", + "qusi8.net", + "qvodzy.org", + "qwant.com", + "qx.net", + "qxbbs.org", + "qz.com", + "r-pool.net", + "r0.ru", + "r10s.jp", + "r18.com", + "ra.gg", + "radicalparty.org", + "radiko.jp", + "radio-canada.ca", + "radio-en-ligne.fr", + "radio.garden", + "radioaustralia.net.au", + "radiohilight.net", + "radioline.co", + "radiotime.com", + "radiovaticana.org", + "radiovncr.com", + "rael.org", + "raggedbanner.com", + "raidcall.com.tw", + "raidtalk.com.tw", + "rainbowplan.org", + "raindrop.io", + "raizoji.or.jp", + "rakuten.co.jp", + "rakuten.com.tw", + "ramcity.com.au", + "rangwang.biz", + "rangzen.com", + "rangzen.net", + "rangzen.org", + "ranxiang.com", + "ranyunfei.com", + "rapbull.net", + "rapidmoviez.com", + "rapidvpn.com", + "rarbgprx.org", + "raremovie.cc", + "raremovie.net", + "rateyourmusic.com", + "rationalwiki.org", + "ratx.com", + "rawgit.com", + "rawgithub.com", + "raxcdn.com", + "razyboard.com", + "rcinet.ca", + "rd.com", + "rdio.com", + "reabble.com", + "read01.com", + "read100.com", + "readingtimes.com.tw", + "readmoo.com", + "readydown.com", + "realcourage.org", + "realitykings.com", + "realraptalk.com", + "realsexpass.com", + "reason.com", + "rebatesrule.net", + "recaptcha.net", + "recordhistory.org", + "recovery.org.tw", + "recoveryversion.com.tw", + "recoveryversion.org", + "red-lang.org", + "redballoonsolidarity.org", + "redbubble.com", + "redchinacn.net", + "redchinacn.org", + "redd.it", + "reddit.com", + "redditlist.com", + "redditmedia.com", + "redditspace.com", + "redditstatic.com", + "redhotlabs.com", + "redtube.com", + "referer.us", + "reflectivecode.com", + "registry.google", + "reimu.net", + "relaxbbs.com", + "relay.com.tw", + "releaseinternational.org", + "religionnews.com", + "religioustolerance.org", + "renminbao.com", + "renyurenquan.org", + "rerouted.org", + "research.google", + "resilio.com", + "resistchina.org", + "retweeteffect.com", + "retweetist.com", + "retweetrank.com", + "reuters.com", + "reutersmedia.net", + "revleft.com", + "revocationcheck.com", + "revver.com", + "rfa.org", + "rfachina.com", + "rfamobile.org", + "rfaweb.org", + "rferl.org", + "rfi.fr", + "rfi.my", + "rightbtc.com", + "rightster.com", + "rigpa.org", + "riku.me", + "rileyguide.com", + "riseup.net", + "ritouki.jp", + "ritter.vg", + "rixcloud.com", + "rixcloud.us", + "rlcdn.com", + "rlwlw.com", + "rmbl.ws", + "rmjdw.com", + "rmjdw132.info", + "roadshow.hk", + "roboforex.com", + "robustnessiskey.com", + "rocket-inc.net", + "rocket.chat", + "rocketbbs.com", + "rocksdb.org", + "rojo.com", + "rolfoundation.org", + "rolia.net", + "rolsociety.org", + "ronjoneswriter.com", + "roodo.com", + "rosechina.net", + "rotten.com", + "rou.video", + "roucdn.link", + "rpglogs.com", + "rsdlmonitor.com", + "rsf-chinese.org", + "rsf.org", + "rsgamen.org", + "rsshub.app", + "rssing.com", + "rssmeme.com", + "rtalabel.org", + "rthk.hk", + "rthk.org.hk", + "rti.org.tw", + "rti.tw", + "rtycminnesota.org", + "ruanyifeng.com", + "rukor.org", + "rule34.xxx", + "rumble.com", + "runbtx.com", + "rushbee.com", + "rusvpn.com", + "ruten.com.tw", + "rutracker.net", + "rutracker.org", + "rutube.ru", + "ruyiseek.com", + "rxhj.net", + "s-cute.com", + "s-dragon.org", + "s-nbcnews.com", + "s1heng.com", + "s1s1s1.com", + "s3-ap-southeast-1.amazonaws.com", + "s3-ap-southeast-2.amazonaws.com", + "s3.amazonaws.com", + "s4miniarchive.com", + "s8forum.com", + "saboom.com", + "sacks.com", + "sacom.hk", + "sadistic-v.com", + "sadpanda.us", + "saeima.lv", + "safechat.com", + "safeguarddefenders.com", + "safervpn.com", + "safety.google", + "sagernet.org", + "saintyculture.com", + "saiq.me", + "sakura-paris.org", + "sakuralive.com", + "sakya.org", + "salvation.org.hk", + "samair.ru", + "sambhota.org", + "sandscotaicentral.com", + "sankakucomplex.com", + "sankei.com", + "sanmin.com.tw", + "sans.edu", + "sapikachu.net", + "saveliuxiaobo.com", + "savemedia.com", + "savethedate.foo", + "savethesounds.info", + "savetibet.de", + "savetibet.fr", + "savetibet.nl", + "savetibet.org", + "savetibet.ru", + "savetibetstore.org", + "saveuighur.org", + "savevid.com", + "say2.info", + "sbme.me", + "sbs.com.au", + "scasino.com", + "schema.org", + "sciencemag.org", + "sciencenets.com", + "scieron.com", + "scmp.com", + "scmpchinese.com", + "scramble.io", + "scribd.com", + "scriptspot.com", + "seapuff.com", + "search.com", + "search.xxx", + "searchtruth.com", + "searx.me", + "seatguru.com", + "seattlefdc.com", + "secretchina.com", + "secretgarden.no", + "secretsline.biz", + "secureservercdn.net", + "securetunnel.com", + "securityinabox.org", + "securitykiss.com", + "see.xxx", + "seed4.me", + "seehua.com", + "seesmic.com", + "seevpn.com", + "seezone.net", + "sehuatang.net", + "sehuatang.org", + "sejie.com", + "seju.life", + "sellclassics.com", + "sendsmtp.com", + "sendspace.com", + "sensortower.com", + "servehttp.com", + "serveuser.com", + "serveusers.com", + "sesawe.net", + "sesawe.org", + "sethwklein.net", + "setn.com", + "settv.com.tw", + "setty.com.tw", + "sevenload.com", + "sex-11.com", + "sex.com", + "sex3.com", + "sex8.cc", + "sexandsubmission.com", + "sexbot.com", + "sexhu.com", + "sexhuang.com", + "sexidude.com", + "sexinsex.net", + "sextvx.com", + "sexxxy.biz", + "sf.net", + "sfileydy.com", + "sfshibao.com", + "sftindia.org", + "sftuk.org", + "sgwritings.com", + "sgzhan.com", + "shadeyouvpn.com", + "shadow.ma", + "shadowsky.xyz", + "shadowsocks-r.com", + "shadowsocks.asia", + "shadowsocks.be", + "shadowsocks.com", + "shadowsocks.com.hk", + "shadowsocks.nu", + "shadowsocks.org", + "shadowsocks9.com", + "shafaqna.com", + "shahit.biz", + "shambalapost.com", + "shambhalasun.com", + "shangfang.org", + "shanxivideo.com", + "shapeservices.com", + "share-videos.se", + "sharebee.com", + "sharecool.org", + "sharpdaily.com.hk", + "sharpdaily.hk", + "sharpdaily.tw", + "shat-tibet.com", + "shattered.io", + "sheet.new", + "sheets.new", + "sheikyermami.com", + "shellfire.de", + "shemalez.com", + "shenshou.org", + "shenyun.com", + "shenyunperformingarts.org", + "shenyunshop.com", + "shenzhoufilm.com", + "shenzhouzhengdao.org", + "sherabgyaltsen.com", + "shiatv.net", + "shicheng.org", + "shiksha.com", + "shiksha.ws", + "shinychan.com", + "shipcamouflage.com", + "shireyishunjian.com", + "shitaotv.org", + "shixiao.org", + "shizhao.org", + "shkspr.mobi", + "shodanhq.com", + "shooshtime.com", + "shop2000.com.tw", + "shopee.tw", + "shopping.com", + "showhaotu.com", + "showtime.jp", + "showwe.tw", + "shutterstock.com", + "shvoong.com", + "shwchurch.org", + "shwchurch3.com", + "siddharthasintent.org", + "sidelinesnews.com", + "sidelinessportseatery.com", + "sierrafriendsoftibet.org", + "signal.org", + "sijihuisuo.club", + "sijihuisuo.com", + "silkbook.com", + "silvergatebank.com", + "simbolostwitter.com", + "simplecd.me", + "simplecd.org", + "simpleproductivityblog.com", + "simpleswap.io", + "sina.com", + "sina.com.hk", + "sina.com.tw", + "sinchew.com.my", + "singaporepools.com.sg", + "singfortibet.com", + "singlelogin.me", + "singlelogin.se", + "singpao.com.hk", + "singtao.ca", + "singtao.com", + "singtaousa.com", + "sino-monthly.com", + "sinoants.com", + "sinoca.com", + "sinocast.com", + "sinocism.com", + "sinoinsider.com", + "sinomontreal.ca", + "sinonet.ca", + "sinopitt.info", + "sinoquebec.com", + "sipml5.org", + "sis.xxx", + "sis001.com", + "sis001.us", + "site.new", + "site2unblock.com", + "site90.net", + "sitebro.tw", + "sitekreator.com", + "sitemaps.org", + "sites.new", + "six-degrees.io", + "sixth.biz", + "sjrt.org", + "sjum.cn", + "sketchappsources.com", + "skimtube.com", + "skk.moe", + "skybet.com", + "skyking.com.tw", + "skykiwi.com", + "skynet.be", + "skype.com", + "skyvegas.com", + "skyxvpn.com", + "sl-reverse.com", + "slacker.com", + "slandr.net", + "slashine.onl", + "slaytizle.com", + "sleazydream.com", + "sleazyfork.org", + "slheng.com", + "slickvpn.com", + "slides.com", + "slides.new", + "slideshare.net", + "slime.com.tw", + "slinkset.com", + "slutload.com", + "slutmoonbeam.com", + "slyip.com", + "slyip.net", + "sm-miracle.com", + "sm3ha.ru", + "smartdnsproxy.com", + "smarthide.com", + "smartmailcloud.com", + "smashwords.com", + "smchbooks.com", + "smh.com.au", + "smhric.org", + "smith.edu", + "smn.news", + "smyxy.org", + "snapchat.com", + "snapseed.com", + "snaptu.com", + "sndcdn.com", + "sneakme.net", + "snow-plus.net", + "snowlionpub.com", + "so-net.net.tw", + "soav.com", + "sobees.com", + "soc.mil", + "social.edu.ci", + "socialblade.com", + "socialwhale.com", + "socks-proxy.net", + "sockscap64.com", + "sockslist.net", + "socrec.org", + "sod.co.jp", + "softether-download.com", + "softether.co.jp", + "softether.org", + "softfamous.com", + "softlayer.net", + "softnology.biz", + "softonic.cn", + "softsmirror.cf", + "softwarebychuck.com", + "sogclub.com", + "sogoo.org", + "sogrady.me", + "soh.tw", + "sohcradio.com", + "sohfrance.org", + "soifind.com", + "sokamonline.com", + "sokmil.com", + "solana.com", + "solidaritetibet.org", + "solidfiles.com", + "solv.finance", + "somee.com", + "songjianjun.com", + "sonicbbs.cc", + "sonidodelaesperanza.org", + "sony.com", + "sopcast.com", + "sopcast.org", + "sophos.com", + "sorazone.net", + "sorting-algorithms.com", + "sos.org", + "sosad.fun", + "sosreader.com", + "sostibet.org", + "sotwe.com", + "sou-tong.org", + "soubory.com", + "soul-plus.net", + "soulcaliburhentai.net", + "soumo.info", + "soundcloud.com", + "soundofhope.kr", + "soundofhope.org", + "soundon.fm", + "soup.io", + "soupofmedia.com", + "sourceforge.net", + "sourcewadio.com", + "south-plus.net", + "south-plus.org", + "southmongolia.org", + "southnews.com.tw", + "sowers.org.hk", + "sowiki.net", + "soylent.com", + "soylentnews.org", + "spankbang.com", + "spankingtube.com", + "spankwire.com", + "sparkpool.com", + "spatial.io", + "spb.com", + "speakerdeck.com", + "speedcat.me", + "speedify.com", + "spem.at", + "spencertipping.com", + "spendee.com", + "spicevpn.com", + "spideroak.com", + "spiderpool.com", + "spiegel.de", + "spike.com", + "spotflux.com", + "spotify.com", + "spreadsheet.new", + "spreadshirt.es", + "spreaker.com", + "spring-plus.net", + "spring4u.info", + "springboardplatform.com", + "springwood.me", + "sprite.org", + "sproutcore.com", + "sproxy.info", + "squirly.info", + "squirrelvpn.com", + "srocket.us", + "ss-link.com", + "ssglobal.co", + "ssglobal.me", + "ssh91.com", + "ssl443.org", + "sspanel.net", + "sspanel.org", + "sspro.ml", + "ssr.tools", + "ssrshare.com", + "ssrshare.us", + "ssrtool.com", + "sss.camp", + "sstm.moe", + "sstmlt.moe", + "sstmlt.net", + "stackcommerce.net", + "stackoverflow.com", + "stacksocial.com", + "stage64.hk", + "standupfortibet.org", + "standwithhk.org", + "stanford.edu", + "starfishfx.com", + "starp2p.com", + "startpage.com", + "startuplivingchina.com", + "stat.gov.tw", + "state.gov", + "static-economist.com", + "statically.io", + "staticflickr.com", + "statsig.com", + "statueofdemocracy.org", + "stboy.net", + "stc.com.sa", + "steamcommunity.com", + "steampowered.com", + "steamstatic.com", + "steel-storm.com", + "steemit.com", + "steganos.com", + "steganos.net", + "stepchina.com", + "stephaniered.com", + "stgloballink.com", + "stheadline.com", + "sthoo.com", + "stickam.com", + "stickeraction.com", + "stileproject.com", + "stitcher.com", + "sto.cc", + "stoporganharvesting.org", + "stoptibetcrisis.net", + "storagenewsletter.com", + "stories.google", + "storify.com", + "storj.io", + "storm.mg", + "stormmediagroup.com", + "stoweboyd.com", + "straitstimes.com", + "stranabg.com", + "straplessdildo.com", + "streamable.com", + "streamate.com", + "streamguys1.com", + "streamingthe.net", + "streema.com", + "streetvoice.com", + "striek.com", + "strikingly.com", + "strongvpn.com", + "strongwindpress.com", + "student.tw", + "studentsforafreetibet.org", + "stumbleupon.com", + "stupidvideos.com", + "stweetly.com", + "substack.com", + "successfn.com", + "sueddeutsche.de", + "sugarsync.com", + "sugobbs.com", + "sugumiru18.com", + "suissl.com", + "sulian.me", + "summify.com", + "sumrando.com", + "sun1911.com", + "sundayguardianlive.com", + "sunmedia.ca", + "suno.ai", + "suno.com", + "sunporno.com", + "sunskyforum.com", + "sunta.com.tw", + "sunvpn.net", + "suoluo.org", + "supchina.com", + "superfreevpn.com", + "superokayama.com", + "superpages.com", + "supervpn.net", + "superzooi.com", + "supjav.com", + "suppig.net", + "suprememastertv.com", + "surfeasy.com", + "surfeasy.com.au", + "surfshark.com", + "suroot.com", + "surrenderat20.net", + "sustainability.google", + "suyangg.com", + "svsfx.com", + "swagbucks.com", + "swapspace.co", + "swbusdev.com", + "swissinfo.ch", + "swissvpn.net", + "switch1.jp", + "switchvpn.net", + "sydneytoday.com", + "sylfoundation.org", + "synapse.org", + "syncback.com", + "synergyse.com", + "syosetu.com", + "sysresccd.org", + "sytes.net", + "syx86.cn", + "syx86.com", + "szbbs.net", + "szetowah.org.hk", + "t-g.com", + "t.co", + "t.me", + "t35.com", + "t35hosting.com", + "t66y.com", + "t91y.com", + "taa-usa.org", + "taaze.tw", + "tablesgenerator.com", + "tabtter.jp", + "tacem.org", + "taconet.com.tw", + "taedp.org.tw", + "tafm.org", + "tagesschau.de", + "tagwa.org.au", + "tagwalk.com", + "tahr.org.tw", + "taipei.gov.tw", + "taipeisociety.org", + "taipeitimes.com", + "taisounds.com", + "taiwan-sex.com", + "taiwanbible.com", + "taiwancon.com", + "taiwandaily.net", + "taiwandc.org", + "taiwanhot.net", + "taiwanjobs.gov.tw", + "taiwanjustice.com", + "taiwanjustice.net", + "taiwankiss.com", + "taiwannation.com", + "taiwannation.com.tw", + "taiwanncf.org.tw", + "taiwannews.com.tw", + "taiwanonline.cc", + "taiwantp.net", + "taiwantt.org.tw", + "taiwanus.net", + "taiwanyes.com", + "talk853.com", + "talkboxapp.com", + "talkcc.com", + "talkonly.net", + "tamiaode.tk", + "tampabay.com", + "tanc.org", + "tangben.com", + "tangren.us", + "taoism.net", + "taolun.info", + "tapanwap.com", + "tapatalk.com", + "taragana.com", + "tardigrade.io", + "target.com", + "tascn.com.au", + "taup.net", + "taup.org.tw", + "taweet.com", + "tbcollege.org", + "tbi.org.hk", + "tbicn.org", + "tbjyt.org", + "tbpic.info", + "tbrc.org", + "tbs-rainbow.org", + "tbsec.org", + "tbsmalaysia.org", + "tbsn.org", + "tbsseattle.org", + "tbssqh.org", + "tbswd.org", + "tbtemple.org.uk", + "tbthouston.org", + "tccwonline.org", + "tcewf.org", + "tchrd.org", + "tcnynj.org", + "tcpspeed.co", + "tcpspeed.com", + "tcsofbc.org", + "tcsovi.org", + "tdesktop.com", + "tdm.com.mo", + "teachparentstech.org", + "teamamericany.com", + "techcrunch.com", + "technews.tw", + "techspot.com", + "techviz.net", + "teck.in", + "teco-hk.org", + "teco-mo.org", + "teddysun.com", + "teenfucks.org", + "teeniefuck.net", + "teensinasia.com", + "teepr.com", + "tehrantimes.com", + "telecomspace.com", + "telegra.ph", + "telegram-cdn.org", + "telegram.dog", + "telegram.me", + "telegram.org", + "telegram.space", + "telegramdownload.com", + "telegraph.co.uk", + "telesco.pe", + "tellme.pw", + "tenacy.com", + "tenor.com", + "tensorflow.org", + "tenzinpalmo.com", + "terabox.com", + "tew.org", + "textnow.com", + "textnow.me", + "tfc-taiwan.org.tw", + "tfhub.dev", + "tfiflve.com", + "tg-me.com", + "tgstat.com", + "thaicn.com", + "thb.gov.tw", + "theage.com.au", + "theatlantic.com", + "theatrum-belli.com", + "theaustralian.com.au", + "theb.ai", + "thebcomplex.com", + "theblaze.com", + "theblemish.com", + "thebobs.com", + "thebodyshop-usa.com", + "thechasernews.co.uk", + "thechinabeat.org", + "thechinacollection.org", + "thechinaproject.com", + "thechinastory.org", + "theconversation.com", + "thedalailamamovie.com", + "thediplomat.com", + "thedw.us", + "theepochtimes.com", + "thefacebook.com", + "thefrontier.hk", + "thegay.com", + "thegioitinhoc.vn", + "thegly.com", + "theguardian.com", + "thehindu.com", + "thehots.info", + "thehousenews.com", + "thehun.net", + "theinitium.com", + "thenewslens.com", + "thepiratebay.ee", + "thepiratebay.org", + "theporndude.com", + "theportalwiki.com", + "theprint.in", + "thereallove.kr", + "therock.net.nz", + "thesaturdaypaper.com.au", + "thespeeder.com", + "thestandard.com.hk", + "thestandnews.com", + "thestar.com", + "thetatoken.org", + "thetibetcenter.org", + "thetibetconnection.org", + "thetibetmuseum.org", + "thetibetpost.com", + "thetrotskymovie.com", + "thetvdb.com", + "thevivekspot.com", + "thewgo.org", + "thewirechina.com", + "theync.com", + "thinkgeek.com", + "thinkhk.com", + "thinkingtaiwan.com", + "thinkwithgoogle.com", + "thirdmill.org", + "thisav.com", + "thlib.org", + "thomasbernhard.org", + "thongdreams.com", + "threadreaderapp.com", + "threads.net", + "threatchaos.com", + "throughnightsfire.com", + "thu.monster", + "thuhole.com", + "thumbzilla.com", + "thywords.com", + "thywords.com.tw", + "tiananmenduizhi.com", + "tiananmenmother.org", + "tiananmenuniv.com", + "tiananmenuniv.net", + "tiandixing.org", + "tianhuayuan.com", + "tianlawoffice.com", + "tianti.io", + "tiantibooks.org", + "tianyantong.org.cn", + "tianzhu.org", + "tibet-envoy.eu", + "tibet-foundation.org", + "tibet-house-trust.co.uk", + "tibet-info.net", + "tibet-initiative.de", + "tibet-munich.de", + "tibet.a.se", + "tibet.at", + "tibet.ca", + "tibet.com", + "tibet.fr", + "tibet.net", + "tibet.nu", + "tibet.org", + "tibet.org.tw", + "tibet.sk", + "tibet.to", + "tibet3rdpole.org", + "tibetaction.net", + "tibetaid.org", + "tibetalk.com", + "tibetan-alliance.org", + "tibetan.fr", + "tibetanaidproject.org", + "tibetanarts.org", + "tibetanbuddhistinstitute.org", + "tibetancommunity.org", + "tibetancommunityuk.net", + "tibetanculture.org", + "tibetanentrepreneurs.org", + "tibetanfeministcollective.org", + "tibetanhealth.org", + "tibetanjournal.com", + "tibetanlanguage.org", + "tibetanliberation.org", + "tibetanpaintings.com", + "tibetanphotoproject.com", + "tibetanpoliticalreview.org", + "tibetanreview.net", + "tibetansports.org", + "tibetanwomen.org", + "tibetanyouth.org", + "tibetanyouthcongress.org", + "tibetcharity.dk", + "tibetcharity.in", + "tibetchild.org", + "tibetcity.com", + "tibetcollection.com", + "tibetcorps.org", + "tibetexpress.net", + "tibetfocus.com", + "tibetfund.org", + "tibetgermany.com", + "tibetgermany.de", + "tibethaus.com", + "tibetheritagefund.org", + "tibethouse.jp", + "tibethouse.org", + "tibethouse.us", + "tibetinfonet.net", + "tibetjustice.org", + "tibetkomite.dk", + "tibetmuseum.org", + "tibetnetwork.org", + "tibetoffice.ch", + "tibetoffice.com.au", + "tibetoffice.eu", + "tibetoffice.org", + "tibetonline.com", + "tibetonline.tv", + "tibetoralhistory.org", + "tibetpolicy.eu", + "tibetrelieffund.co.uk", + "tibetsites.com", + "tibetsociety.com", + "tibetsun.com", + "tibetsupportgroup.org", + "tibetswiss.ch", + "tibettelegraph.com", + "tibettimes.net", + "tibettruth.com", + "tibetwrites.org", + "ticket.com.tw", + "tigervpn.com", + "tiktok.com", + "tiktokcdn-us.com", + "tiktokcdn.com", + "tiktokv.com", + "tiktokv.us", + "tiltbrush.com", + "timdir.com", + "time.com", + "timesnownews.com", + "timsah.com", + "timtales.com", + "tinc-vpn.org", + "tiney.com", + "tineye.com", + "tingtalk.me", + "tintuc101.com", + "tiny.cc", + "tinychat.com", + "tinypaste.com", + "tinyurl.com", + "tipas.net", + "tipo.gov.tw", + "tistory.com", + "tkcs-collins.com", + "tl.gd", + "tma.co.jp", + "tmagazine.com", + "tmdfish.com", + "tmi.me", + "tmpp.org", + "tnaflix.com", + "tngrnow.com", + "tngrnow.net", + "tnp.org", + "to-porno.com", + "togetter.com", + "toh.info", + "token.im", + "tokenlon.im", + "tokyo-247.com", + "tokyo-hot.com", + "tokyo-porn-tube.com", + "tokyocn.com", + "tomonews.net", + "tomp3.cc", + "tongil.or.kr", + "tono-oka.jp", + "tonyyan.net", + "toodoc.com", + "toonel.net", + "top.tv", + "top10vpn.com", + "top81.ws", + "topbtc.com", + "topnews.in", + "toppornsites.com", + "topshareware.com", + "topsy.com", + "toptip.ca", + "toptoon.net", + "tora.to", + "torcn.com", + "torguard.net", + "torlock.com", + "torproject.org", + "torrentgalaxy.to", + "torrentkitty.tv", + "torrentprivacy.com", + "torrentproject.se", + "torrenty.org", + "torrentz.eu", + "tortoisesvn.net", + "torvpn.com", + "totalvpn.com", + "tou.tv", + "toutiaoabc.com", + "towngain.com", + "toypark.in", + "toythieves.com", + "toytractorshow.com", + "tparents.org", + "tpi.org.tw", + "tracfone.com", + "tradingview.com", + "traffichaus.com", + "translate.goog", + "transparency.org", + "travelinkcard.com", + "treemall.com.tw", + "trendsmap.com", + "trialofccp.org", + "trickip.net", + "trickip.org", + "trimondi.de", + "tron.network", + "tronscan.org", + "trouw.nl", + "trt.net.tr", + "trtc.com.tw", + "truebuddha-md.org", + "trulyergonomic.com", + "truthontour.org", + "truthsocial.com", + "truveo.com", + "tryheart.jp", + "tsctv.net", + "tsemtulku.com", + "tsquare.tv", + "tsu.org.tw", + "tsunagarumon.com", + "tt1069.com", + "tttan.com", + "ttv.com.tw", + "ttvnw.net", + "ttwstatic.com", + "tu8964.com", + "tubaholic.com", + "tube.com", + "tube8.com", + "tube911.com", + "tubecup.com", + "tubegals.com", + "tubeislam.com", + "tubepornclassic.com", + "tubestack.com", + "tubewolf.com", + "tuibeitu.net", + "tuidang.net", + "tuidang.org", + "tuidang.se", + "tuitui.info", + "tuitwit.com", + "tukaani.org", + "tumblr.com", + "tumutanzi.com", + "tumview.com", + "tunein.com", + "tunnelbear.com", + "tunnelblick.net", + "tunnelr.com", + "tunsafe.com", + "tuo8.blue", + "tuo8.cc", + "tuo8.club", + "tuo8.fit", + "tuo8.hk", + "tuo8.in", + "tuo8.ninja", + "tuo8.org", + "tuo8.pw", + "tuo8.red", + "tuo8.space", + "turansam.org", + "turbobit.net", + "turbohide.com", + "turbotwitter.com", + "turkistantimes.com", + "turntable.fm", + "tushycash.com", + "tutanota.com", + "tuvpn.com", + "tuzaijidi.com", + "tv.com", + "tv.google", + "tvants.com", + "tvb.com", + "tvboxnow.com", + "tvbs.com.tw", + "tvider.com", + "tvmost.com.hk", + "tvplayvideos.com", + "tvunetworks.com", + "tw-blog.com", + "tw-npo.org", + "tw01.org", + "twaitter.com", + "twapperkeeper.com", + "twaud.io", + "twavi.com", + "twbbs.net.tw", + "twbbs.org", + "twbbs.tw", + "twblogger.com", + "twcomix.com", + "tweepguide.com", + "tweeplike.me", + "tweepmag.com", + "tweepml.org", + "tweetbackup.com", + "tweetboard.com", + "tweetboner.biz", + "tweetcs.com", + "tweetdeck.com", + "tweetedtimes.com", + "tweetmylast.fm", + "tweetphoto.com", + "tweetrans.com", + "tweetree.com", + "tweettunnel.com", + "tweetwally.com", + "tweetymail.com", + "tweez.net", + "twelve.today", + "twerkingbutt.com", + "twftp.org", + "twgreatdaily.com", + "twibase.com", + "twibble.de", + "twibbon.com", + "twibs.com", + "twicountry.org", + "twicsy.com", + "twiends.com", + "twifan.com", + "twiffo.com", + "twiggit.org", + "twilightsex.com", + "twilio.com", + "twilog.org", + "twimbow.com", + "twimg.com", + "twindexx.com", + "twip.me", + "twipple.jp", + "twishort.com", + "twistar.cc", + "twister.net.co", + "twisterio.com", + "twisternow.com", + "twistory.net", + "twit2d.com", + "twitbrowser.net", + "twitcause.com", + "twitch.tv", + "twitchcdn.net", + "twitgether.com", + "twitgoo.com", + "twitiq.com", + "twitlonger.com", + "twitmania.com", + "twitoaster.com", + "twitonmsn.com", + "twitpic.com", + "twitstat.com", + "twittbot.net", + "twitter.com", + "twitter.jp", + "twitter4j.org", + "twittercounter.com", + "twitterfeed.com", + "twittergadget.com", + "twitterkr.com", + "twittermail.com", + "twitterrific.com", + "twittertim.es", + "twitthat.com", + "twitturk.com", + "twitturly.com", + "twitvid.com", + "twitzap.com", + "twiyia.com", + "twkan.com", + "twnorth.org.tw", + "twreporter.org", + "twskype.com", + "twstar.net", + "twt.tl", + "twtkr.com", + "twtrland.com", + "twttr.com", + "twurl.nl", + "twyac.org", + "txxx.com", + "tycool.com", + "typekit.net", + "typepad.com", + "typeset.io", + "typora.io", + "u15.info", + "u9un.com", + "ua5v.com", + "ub0.cc", + "ubddns.org", + "uberproxy.net", + "uc-japan.org", + "ucam.org", + "ucanews.com", + "ucdc1998.org", + "uchicago.edu", + "uderzo.it", + "udn.com", + "udn.com.tw", + "udnbkk.com", + "udndata.com", + "uforadio.com.tw", + "ufreevpn.com", + "ugo.com", + "uhdwallpapers.org", + "uhrp.org", + "uighur.nl", + "uighurbiz.net", + "uk.to", + "ukcdp.co.uk", + "ukliferadio.co.uk", + "uku.im", + "ulifestyle.com.hk", + "ulike.net", + "ulop.net", + "ultrasurf.us", + "ultravpn.com", + "ultravpn.fr", + "ultraxs.com", + "umich.edu", + "unblock-us.com", + "unblock.cn.com", + "unblockdmm.com", + "unblocker.yt", + "unblocksit.es", + "uncyclomedia.org", + "uncyclopedia.hk", + "uncyclopedia.tw", + "underwoodammo.com", + "unholyknight.com", + "uni.cc", + "unicode.org", + "unification.net", + "unification.org.tw", + "unirule.cloud", + "unitedsocialpress.com", + "unix100.com", + "unknownspace.org", + "unlock-music.dev", + "unmineable.com", + "unodedos.com", + "unpo.org", + "unseen.is", + "unstable.icu", + "untraceable.us", + "unwire.hk", + "uocn.org", + "updatestar.com", + "upghsbc.com", + "upholdjustice.org", + "upload4u.info", + "uploaded.net", + "uploaded.to", + "uploadstation.com", + "upmedia.mg", + "upornia.com", + "uproxy.org", + "uptodown.com", + "upwill.org", + "ur7s.com", + "uraban.me", + "urbandictionary.com", + "urbansurvival.com", + "urchin.com", + "url.com.tw", + "urlborg.com", + "urlparser.com", + "us.to", + "usacn.com", + "usaip.eu", + "usc.edu", + "uscardforum.com", + "uscg.mil", + "uscnpm.org", + "usefreevpn.com", + "usembassy.gov", + "usercontent.goog", + "usfk.mil", + "usma.edu", + "usmc.mil", + "usocctn.com", + "uspto.gov", + "ustibetcommittee.org", + "ustream.tv", + "usunitednews.com", + "usus.cc", + "utopianpal.com", + "uu-gg.com", + "uujiasu.com", + "uukanshu.com", + "uupool.cn", + "uvwxyz.xyz", + "uwants.com", + "uwants.net", + "uyghur-archive.com", + "uyghur-j.org", + "uyghur.co.uk", + "uyghuraa.org", + "uyghuramerican.org", + "uyghurbiz.org", + "uyghurcanadian.ca", + "uyghurcanadiansociety.org", + "uyghurcongress.org", + "uyghurensemble.co.uk", + "uyghurpen.org", + "uyghurpress.com", + "uyghurstudies.org", + "uyghurtribunal.com", + "uygur.org", + "uymaarip.com", + "v2.help", + "v2board.com", + "v2ex.com", + "v2fly.org", + "v2mm.tech", + "v2ray.com", + "v2raya.org", + "v2raycn.com", + "v2raytech.com", + "valeursactuelles.com", + "van001.com", + "van698.com", + "vanemu.cn", + "vanilla-jp.com", + "vanpeople.com", + "vansky.com", + "vaticannews.va", + "vatn.org", + "vcf-online.org", + "vcfbuilder.org", + "vegasred.com", + "velkaepocha.sk", + "venbbs.com", + "venchina.com", + "venetianmacao.com", + "ventureswell.com", + "veoh.com", + "vercel.app", + "vercel.com", + "verizon.net", + "vermonttibet.org", + "vern.cc", + "versavpn.com", + "verybs.com", + "vevo.com", + "vewas.net", + "vft.com.tw", + "viber.com", + "vica.info", + "victimsofcommunism.org", + "vid.me", + "vidble.com", + "videobam.com", + "videodetective.com", + "videomega.tv", + "videomo.com", + "videopediaworld.com", + "videopress.com", + "vidinfo.org", + "vietdaikynguyen.com", + "vijayatemple.org", + "vilanet.me", + "vilavpn.com", + "vimeo.com", + "vimeocdn.com", + "vimperator.org", + "vincnd.com", + "vine.co", + "vinniev.com", + "vip-enterprise.com", + "virginia.edu", + "virtualrealporn.com", + "visibletweets.com", + "visiontimes.com", + "visualstudio.com", + "vital247.org", + "viu.com", + "viu.tv", + "vivahentai4u.net", + "vivaldi.com", + "vivatube.com", + "vivthomas.com", + "vizvaz.com", + "vjav.com", + "vjmedia.com.hk", + "vllcs.org", + "vmixcore.com", + "vmpsoft.com", + "vnet.link", + "voa.mobi", + "voacambodia.com", + "voacantonese.com", + "voachinese.com", + "voachineseblog.com", + "voagd.com", + "voaindonesia.com", + "voanews.com", + "voatibetan.com", + "voatibetanenglish.com", + "vocaroo.com", + "vocativ.com", + "vocn.tv", + "vocus.cc", + "voi.id", + "voicettank.org", + "vot.org", + "vovo2000.com", + "vox.com", + "voxer.com", + "voy.com", + "vpn.ac", + "vpn.net", + "vpn4all.com", + "vpnaccount.org", + "vpnaccounts.com", + "vpnbook.com", + "vpncomparison.org", + "vpncoupons.com", + "vpncup.com", + "vpndada.com", + "vpnfan.com", + "vpnfire.com", + "vpnfires.biz", + "vpnforgame.net", + "vpngate.jp", + "vpngate.net", + "vpngratis.net", + "vpnhq.com", + "vpnhub.com", + "vpninja.net", + "vpnintouch.com", + "vpnintouch.net", + "vpnjack.com", + "vpnmaster.com", + "vpnmentor.com", + "vpnpick.com", + "vpnpop.com", + "vpnpronet.com", + "vpnproxymaster.com", + "vpnreactor.com", + "vpnreviewz.com", + "vpnsecure.me", + "vpnshazam.com", + "vpnshieldapp.com", + "vpnsp.com", + "vpntraffic.com", + "vpntunnel.com", + "vpnuk.info", + "vpnunlimitedapp.com", + "vpnvip.com", + "vpnworldwide.com", + "vporn.com", + "vpser.net", + "vraiesagesse.net", + "vrchat.com", + "vrmtr.com", + "vrporn.com", + "vrsmash.com", + "vs.com", + "vtunnel.com", + "vuku.cc", + "vultryhw.com", + "vyprvpn.com", + "vzw.com", + "w-pool.com", + "w.wiki", + "w3.org", + "waffle1999.com", + "wahas.com", + "waigaobu.com", + "waikeung.org", + "wailaike.net", + "wainao.me", + "waiwaier.com", + "wallhaven.cc", + "wallmama.com", + "wallornot.org", + "wallpapercasa.com", + "wallproxy.com", + "wallsttv.com", + "waltermartin.com", + "waltermartin.org", + "wan-press.org", + "wanderinghorse.net", + "wangafu.net", + "wangjinbo.org", + "wanglixiong.com", + "wango.org", + "wangruoshui.net", + "wangruowang.org", + "want-daily.com", + "wanz-factory.com", + "wapedia.mobi", + "warehouse333.com", + "warroom.org", + "waselpro.com", + "washeng.net", + "washingtonpost.com", + "watch8x.com", + "watchinese.com", + "watchmygf.net", + "watchout.tw", + "wattpad.com", + "wav.tv", + "waveprotocol.org", + "waybig.com", + "waymo.com", + "wd.bible", + "wda.gov.tw", + "wdf5.com", + "wealth.com.tw", + "wearehairy.com", + "wearn.com", + "weather.com.hk", + "web.dev", + "web2project.net", + "webbang.net", + "webevader.org", + "webfreer.com", + "webjb.org", + "weblagu.com", + "webmproject.org", + "webpack.de", + "webpkgcache.com", + "webrtc.org", + "webrush.net", + "webs-tv.net", + "website.new", + "websitepulse.com", + "websnapr.com", + "webwarper.net", + "webworkerdaily.com", + "wechatlawsuit.com", + "weebly.com", + "weekmag.info", + "wefightcensorship.org", + "wefong.com", + "weiboleak.com", + "weihuo.org", + "weijingsheng.org", + "weiming.info", + "weiquanwang.org", + "weisuo.ws", + "weitt.us", + "welovecock.com", + "welt.de", + "wemigrate.org", + "wengewang.com", + "wengewang.org", + "wenhui.ch", + "wenweipo.com", + "wenxuecity.com", + "wenyunchao.com", + "wenzhao.ca", + "westca.com", + "westernshugdensociety.org", + "westernwolves.com", + "westkit.net", + "westpoint.edu", + "wetplace.com", + "wetpussygames.com", + "wexiaobo.org", + "wezhiyong.org", + "wezone.net", + "wforum.com", + "wha.la", + "whatblocked.com", + "whatbrowser.org", + "whats.new", + "whatsapp.com", + "whatsapp.net", + "whatsonweibo.com", + "wheatseeds.org", + "wheelockslatin.com", + "whereiswerner.com", + "wheretowatch.com", + "whichav.com", + "whichav.video", + "whippedass.com", + "whispersystems.org", + "who.is", + "whodns.xyz", + "whoer.net", + "whotalking.com", + "whylover.com", + "whyx.org", + "widevine.com", + "wikaba.com", + "wikia.com", + "wikia.org", + "wikibooks.org", + "wikidata.org", + "wikileaks-forum.com", + "wikileaks.ch", + "wikileaks.com", + "wikileaks.de", + "wikileaks.eu", + "wikileaks.lu", + "wikileaks.org", + "wikileaks.pl", + "wikilivres.info", + "wikimapia.org", + "wikimedia.org", + "wikinews.org", + "wikipedia-on-ipfs.org", + "wikipedia.org", + "wikiquote.org", + "wikisource.org", + "wikiversity.org", + "wikivoyage.org", + "wikiwand.com", + "wiktionary.org", + "wildammo.com", + "williamhill.com", + "willw.net", + "wilsoncenter.org", + "windowsphoneme.com", + "windscribe.com", + "windy.com", + "wingamestore.com", + "wingy.site", + "winning11.com", + "winwhispers.info", + "wionews.com", + "wire.com", + "wiredbytes.com", + "wiredpen.com", + "wireguard.com", + "wisdompubs.org", + "wisevid.com", + "wistia.com", + "withgoogle.com", + "withyoutube.com", + "witnessleeteaching.com", + "witopia.net", + "wixsite.com", + "wizcrafts.net", + "wjbk.org", + "wmflabs.org", + "wmfusercontent.org", + "wn.com", + "wnacg.com", + "wnacg.org", + "wo.tc", + "woeser.com", + "wokar.org", + "wolfax.com", + "wombo.ai", + "woolyss.com", + "woopie.jp", + "woopie.tv", + "wordpress.com", + "work2icu.org", + "workatruna.com", + "workerdemo.org.hk", + "workerempowerment.org", + "workers.dev", + "workersthebig.net", + "workflow.is", + "worldcat.org", + "worldjournal.com", + "worldpopulationreview.com", + "worldvpn.net", + "wow-life.net", + "wow.com", + "wowgirls.com", + "wowhead.com", + "wowlegacy.ml", + "wowporn.com", + "wowrk.com", + "woxinghuiguo.com", + "woyaolian.org", + "wozy.in", + "wp.com", + "wpoforum.com", + "wqyd.org", + "wrchina.org", + "wretch.cc", + "writesonic.com", + "wsimg.com", + "wsj.com", + "wsj.net", + "wsjhk.com", + "wtbn.org", + "wtfpeople.com", + "wuerkaixi.com", + "wufafangwen.com", + "wufi.org.tw", + "wuguoguang.com", + "wujie.net", + "wujieliulan.com", + "wukangrui.net", + "wunderground.com", + "wuw.red", + "wuyanblog.com", + "wwe.com", + "wwitv.com", + "www1.biz", + "wwwhost.biz", + "wzyboy.im", + "x-art.com", + "x-berry.com", + "x-wall.org", + "x.ai", + "x.co", + "x.com", + "x.company", + "x1949x.com", + "x24hr.com", + "x365x.com", + "x3guide.com", + "xaislam.com", + "xanga.com", + "xbabe.com", + "xbookcn.com", + "xbtce.com", + "xcafe.in", + "xchina.co", + "xcity.jp", + "xcritic.com", + "xerotica.com", + "xfiles.to", + "xfinity.com", + "xfxssr.me", + "xgmyd.com", + "xhamster.com", + "xhcdn.com", + "xianba.net", + "xianchawang.net", + "xianjian.tw", + "xianqiao.net", + "xiaobaiwu.com", + "xiaochuncnjp.com", + "xiaod.in", + "xiaohexie.com", + "xiaolan.me", + "xiaoma.org", + "xiaomi.eu", + "xiaxiaoqiang.net", + "xiezhua.com", + "xihua.es", + "xinbao.de", + "xing.com", + "xinhuanet.org", + "xinjiangpolicefiles.org", + "xinmiao.com.hk", + "xinsheng.net", + "xinshijue.com", + "xinyubbs.net", + "xiongpian.com", + "xiuren.org", + "xixicui.icu", + "xizang-zhiye.org", + "xjp.cc", + "xjtravelguide.com", + "xkiwi.tk", + "xlfmtalk.com", + "xlfmwz.info", + "xm.com", + "xml-training-guide.com", + "xmonk.net", + "xmovies.com", + "xn--11xs86f.icu", + "xn--4gq171p.com", + "xn--90wwvt03e.com", + "xn--9pr62r24a.com", + "xn--czq75pvv1aj5c.org", + "xn--i2ru8q2qg.com", + "xn--ngstr-lra8j.com", + "xn--noss43i.com", + "xn--oiq.cc", + "xnpool.com", + "xnxx-cdn.com", + "xnxx.com", + "xpdo.net", + "xpud.org", + "xrentdvd.com", + "xsden.info", + "xsden.org", + "xskywalker.com", + "xskywalker.net", + "xt.com", + "xt.pub", + "xtube.com", + "xuchao.net", + "xuchao.org", + "xuehua.us", + "xuite.net", + "xuzhiyong.net", + "xvbelink.com", + "xvideo.cc", + "xvideos-cdn.com", + "xvideos.com", + "xvideos.es", + "xvideos2.com", + "xvinlink.com", + "xxbbx.com", + "xxlmovies.com", + "xxuz.com", + "xxx.com", + "xxx.xxx", + "xxxfuckmom.com", + "xxxx.com.au", + "xxxy.biz", + "xxxy.info", + "xxxymovies.com", + "xys.org", + "xysblogs.org", + "xyy69.com", + "xyy69.info", + "y2mate.com", + "yadi.sk", + "yahoo.co.jp", + "yahoo.com", + "yahoo.com.hk", + "yahoo.com.tw", + "yahoo.net", + "yahoosandbox.com", + "yakbutterblues.com", + "yam.com", + "yam.org.tw", + "yande.re", + "yandex.com", + "yandex.net", + "yandex.ru", + "yanghengjun.com", + "yangjianli.com", + "yasni.co.uk", + "yasukuni.or.jp", + "yayabay.com", + "ycombinator.com", + "ydy.com", + "yeahteentube.com", + "yecl.net", + "yeelou.com", + "yeeyi.com", + "yegle.net", + "yes-news.com", + "yes.xxx", + "yes123.com.tw", + "yesasia.com", + "yesasia.com.hk", + "yespornplease.com", + "yeyeclub.com", + "ygto.com", + "yhcw.net", + "yibada.com", + "yibaochina.com", + "yidio.com", + "yigeni.com", + "yilubbs.com", + "yimg.com", + "yingsuoss.com", + "yinlei.org", + "yipub.com", + "yizhihongxing.com", + "yobit.net", + "yobt.com", + "yobt.tv", + "yogichen.org", + "yolasite.com", + "yomiuri.co.jp", + "yong.hu", + "yorkbbs.ca", + "you-get.org", + "you.com", + "youdontcare.com", + "youjizz.com", + "youlucky.com", + "youmaker.com", + "youngpornvideos.com", + "youngspiration.hk", + "youpai.org", + "youporn.com", + "youporngay.com", + "your-freedom.net", + "yourepeat.com", + "yourlisten.com", + "yourlust.com", + "yourprivatevpn.com", + "yourtrap.com", + "yousendit.com", + "youshun12.com", + "youthforfreechina.org", + "youthnetradio.org", + "youthwant.com.tw", + "youtu.be", + "youtube-nocookie.com", + "youtube.com", + "youtubecn.com", + "youtubeeducation.com", + "youtubegaming.com", + "youtubekids.com", + "youversion.com", + "youwin.com", + "youxu.info", + "yt.be", + "ytht.net", + "ytimg.com", + "ytn.co.kr", + "yts.ag", + "yts.am", + "yts.lt", + "yts.mx", + "yuanming.net", + "yuanzhengtang.org", + "yulghun.com", + "yunchao.net", + "yunomi.tokyo", + "yuntipub.com", + "yuvutu.com", + "yvesgeleyn.com", + "ywpw.com", + "yx51.net", + "yyii.org", + "yyjlymb.xyz", + "yysub.net", + "yzzk.com", + "z-lib.io", + "z-lib.org", + "zacebook.com", + "zalmos.com", + "zamimg.com", + "zannel.com", + "zaobao.com.sg", + "zaozon.com", + "zapto.org", + "zattoo.com", + "zb.com", + "zdnet.com.tw", + "zello.com", + "zengjinyan.org", + "zenmate.com", + "zenmate.com.ru", + "zergpool.com", + "zerohedge.com", + "zeronet.io", + "zeutch.com", + "zfreet.com", + "zgsddh.com", + "zgzcjj.net", + "zhanbin.net", + "zhangboli.net", + "zhangtianliang.com", + "zhanlve.org", + "zhenghui.org", + "zhengjian.org", + "zhengwunet.org", + "zhenlibu.info", + "zhenlibu1984.com", + "zhenxiang.biz", + "zhinengluyou.com", + "zhizhu.top", + "zhongguo.ca", + "zhongguorenquan.org", + "zhongguotese.net", + "zhongmeng.org", + "zhongzidi.com", + "zhoushuguang.com", + "zhreader.com", + "zhuangbi.me", + "zhuanxing.cn", + "zhuatieba.com", + "zhuichaguoji.org", + "zhujiget.com", + "zi.media", + "zi5.me", + "ziddu.com", + "zillionk.com", + "zim.vn", + "zinio.com", + "ziporn.com", + "zippyshare.com", + "zkaip.com", + "zkiz.com", + "zmedia.com.tw", + "zmw.cn", + "zodgame.us", + "zoho.com", + "zomobo.net", + "zonaeuropa.com", + "zonghexinwen.com", + "zonghexinwen.net", + "zoogvpn.com", + "zoominfo.com", + "zooqle.com", + "zootool.com", + "zoozle.net", + "zophar.net", + "zorrovpn.com", + "zozotown.com", + "zpn.im", + "zsdxzk.com", + "zspeeder.me", + "zsrhao.com", + "zuo.la", + "zuobiao.me", + "zuola.com", + "zvereff.com", + "zynaima.com", + "zynamics.com", + "zyns.com", + "zyxel.com", + "zyzc9.com", + "zzcartoon.com", + "zzcloud.me", + "zzux.com" + ] + ], + [ + [], + [] + ] +]; + +var lastRule = ''; + +function FindProxyForURL(url, host) { + for (var i = 0; i < rules.length; i++) { + ret = testHost(host, i); + if (ret != undefined) + return ret; + } + return 'DIRECT'; +} + +function testHost(host, index) { + for (var i = 0; i < rules[index].length; i++) { + for (var j = 0; j < rules[index][i].length; j++) { + lastRule = rules[index][i][j] + if (host == lastRule || host.endsWith('.' + lastRule)) + return i % 2 == 0 ? 'DIRECT' : proxy; + } + } + lastRule = ''; +} + +// REF: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith +if (!String.prototype.endsWith) { + String.prototype.endsWith = function(searchString, position) { + var subjectString = this.toString(); + if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) { + position = subjectString.length; + } + position -= searchString.length; + var lastIndex = subjectString.indexOf(searchString, position); + return lastIndex !== -1 && lastIndex === position; + }; +} diff --git a/v2rayN/ServiceLib/Sample/proxy_set_linux_sh b/v2rayN/ServiceLib/Sample/proxy_set_linux_sh new file mode 100644 index 00000000..112bb39f --- /dev/null +++ b/v2rayN/ServiceLib/Sample/proxy_set_linux_sh @@ -0,0 +1,157 @@ +#!/bin/bash + +# Function to set proxy for GNOME +set_gnome_proxy() { + local MODE=$1 + local PROXY_IP=$2 + local PROXY_PORT=$3 + local IGNORE_HOSTS=$4 + + # Set the proxy mode + gsettings set org.gnome.system.proxy mode "$MODE" + + if [ "$MODE" == "manual" ]; then + # List of protocols + local PROTOCOLS=("http" "https" "ftp" "socks") + + # Loop through protocols to set the proxy + for PROTOCOL in "${PROTOCOLS[@]}"; do + gsettings set org.gnome.system.proxy.$PROTOCOL host "$PROXY_IP" + gsettings set org.gnome.system.proxy.$PROTOCOL port "$PROXY_PORT" + done + + # Set ignored hosts + gsettings set org.gnome.system.proxy ignore-hosts "['$IGNORE_HOSTS']" + + echo "GNOME: Manual proxy settings applied." + echo "Proxy IP: $PROXY_IP" + echo "Proxy Port: $PROXY_PORT" + echo "Ignored Hosts: $IGNORE_HOSTS" + elif [ "$MODE" == "none" ]; then + echo "GNOME: Proxy disabled." + fi +} + +# Function to set proxy for KDE +set_kde_proxy() { + local MODE=$1 + local PROXY_IP=$2 + local PROXY_PORT=$3 + local IGNORE_HOSTS=$4 + + # Determine the correct kwriteconfig command based on KDE_SESSION_VERSION + if [ "$KDE_SESSION_VERSION" == "6" ]; then + KWRITECONFIG="kwriteconfig6" + else + KWRITECONFIG="kwriteconfig5" + fi + + # KDE uses kwriteconfig to modify proxy settings + if [ "$MODE" == "manual" ]; then + # Set proxy for all protocols + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key ProxyType 1 + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key httpProxy "http://$PROXY_IP:$PROXY_PORT" + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key httpsProxy "http://$PROXY_IP:$PROXY_PORT" + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key ftpProxy "http://$PROXY_IP:$PROXY_PORT" + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key socksProxy "http://$PROXY_IP:$PROXY_PORT" + + # Set ignored hosts + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key NoProxyFor "$IGNORE_HOSTS" + + echo "KDE: Manual proxy settings applied." + echo "Proxy IP: $PROXY_IP" + echo "Proxy Port: $PROXY_PORT" + echo "Ignored Hosts: $IGNORE_HOSTS" + elif [ "$MODE" == "none" ]; then + # Disable proxy + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key ProxyType 0 + echo "KDE: Proxy disabled." + fi + + # Apply changes by restarting KDE's network settings + dbus-send --type=signal /KIO/Scheduler org.kde.KIO.Scheduler.reparseSlaveConfiguration string:"" +} + +# Detect the current desktop environment +detect_desktop_environment() { + if [[ "$XDG_CURRENT_DESKTOP" == *"GNOME"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"GNOME"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"XFCE"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"XFCE"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"X-Cinnamon"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"cinnamon"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"UKUI"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"ukui"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"DDE"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"dde"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"MATE"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"mate"* ]]; then + echo "gnome" + return + fi + + local KDE_ENVIRONMENTS=("KDE" "plasma") + for ENV in "${KDE_ENVIRONMENTS[@]}"; do + if [ "$XDG_CURRENT_DESKTOP" == "$ENV" ] || [ "$XDG_SESSION_DESKTOP" == "$ENV" ]; then + echo "kde" + return + fi + done + + # Fallback to GNOME method if CLI utility is available. This solves the + # proxy configuration issues on minimal installation systems, like setups + # with only window managers, that borrow some parts from big DEs. + if command -v gsettings >/dev/null 2>&1; then + echo "gnome" + return + fi + + echo "unsupported" +} + +# Main script logic +if [ "$#" -lt 1 ]; then + echo "Usage: $0 [proxy_ip proxy_port ignore_hosts]" + echo " mode: 'none' or 'manual'" + echo " If mode is 'manual', provide proxy IP, port, and ignore hosts." + exit 1 +fi + +# Get the mode +MODE=$1 +PROXY_IP=$2 +PROXY_PORT=$3 +IGNORE_HOSTS=$4 + +if ! [[ "$MODE" =~ ^(manual|none)$ ]]; then + echo "Invalid mode. Use 'none' or 'manual'." >&2 + exit 1 +fi + +# Detect desktop environment +DE=$(detect_desktop_environment) + +# Apply settings based on the desktop environment +if [ "$DE" == "gnome" ]; then + set_gnome_proxy "$MODE" "$PROXY_IP" "$PROXY_PORT" "$IGNORE_HOSTS" +elif [ "$DE" == "kde" ]; then + set_gnome_proxy "$MODE" "$PROXY_IP" "$PROXY_PORT" "$IGNORE_HOSTS" + set_kde_proxy "$MODE" "$PROXY_IP" "$PROXY_PORT" "$IGNORE_HOSTS" +else + echo "Unsupported desktop environment: $DE" >&2 + exit 1 +fi diff --git a/v2rayN/ServiceLib/Sample/proxy_set_osx_sh b/v2rayN/ServiceLib/Sample/proxy_set_osx_sh new file mode 100644 index 00000000..90d0119b --- /dev/null +++ b/v2rayN/ServiceLib/Sample/proxy_set_osx_sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# Function to set proxy +set_proxy() { + PROXY_IP=$1 + PROXY_PORT=$2 + + shift 2 + BYPASS_DOMAINS=("$@") + # If no bypass domains are provided, set it to empty by default + if [ ${#BYPASS_DOMAINS[@]} -eq 0 ]; then + BYPASS_DOMAINS=("") + fi + + # Get all network service names + SERVICES=$(networksetup -listallnetworkservices | grep -v '*') + + # Loop through each network service + echo "$SERVICES" | while read -r SERVICE; do + echo "Setting proxy for network service '$SERVICE'..." + # Set HTTP proxy + networksetup -setwebproxy "$SERVICE" "$PROXY_IP" "$PROXY_PORT" + + # Set HTTPS proxy + networksetup -setsecurewebproxy "$SERVICE" "$PROXY_IP" "$PROXY_PORT" + + # Set SOCKS proxy + networksetup -setsocksfirewallproxy "$SERVICE" "$PROXY_IP" "$PROXY_PORT" + + # Set bypass domains + networksetup -setproxybypassdomains "$SERVICE" "${BYPASS_DOMAINS[@]}" + echo "Proxy for network service '$SERVICE' has been set to $PROXY_IP:$PROXY_PORT" + done + echo "Proxy settings for all network services are complete!" +} + +# Function to disable proxy +clear_proxy() { + # Get all network service names + SERVICES=$(networksetup -listallnetworkservices | grep -v '*') + + # Loop through each network service + echo "$SERVICES" | while read -r SERVICE; do + echo "Disabling proxy and clearing bypass domains for network service '$SERVICE'..." + # Disable HTTP proxy + networksetup -setwebproxystate "$SERVICE" off + + # Disable HTTPS proxy + networksetup -setsecurewebproxystate "$SERVICE" off + + # Disable SOCKS proxy + networksetup -setsocksfirewallproxystate "$SERVICE" off + + echo "Proxy for network service '$SERVICE' has been disabled" + done + echo "Proxy for all network services has been disabled!" +} + +# Main script logic +if [ "$1" == "set" ]; then + # Check if enough parameters are passed for setting proxy + if [ "$#" -lt 3 ]; then + echo "Usage: $0 set [Bypass Domain 1 Bypass Domain 2 ...]" + exit 1 + fi + set_proxy "$2" "$3" "${@:4}" +elif [ "$1" == "clear" ]; then + clear_proxy +else + echo "Usage:" + echo " To set proxy: $0 set [Bypass Domain 1 Bypass Domain 2 ...]" + echo " To clear proxy: $0 clear" + exit 1 +fi diff --git a/v2rayN/ServiceLib/Sample/tun_singbox_dns b/v2rayN/ServiceLib/Sample/tun_singbox_dns new file mode 100644 index 00000000..3dd55eef --- /dev/null +++ b/v2rayN/ServiceLib/Sample/tun_singbox_dns @@ -0,0 +1,34 @@ +{ + "servers": [ + { + "tag": "remote", + "type": "tcp", + "server": "8.8.8.8", + "detour": "proxy" + }, + { + "tag": "local", + "type": "udp", + "server": "223.5.5.5" + } + ], + "rules": [ + { + "domain_suffix": [ + "googleapis.cn", + "gstatic.com" + ], + "server": "remote", + "strategy": "prefer_ipv4" + }, + { + "rule_set": [ + "geosite-cn" + ], + "server": "local", + "strategy": "prefer_ipv4" + } + ], + "final": "remote", + "strategy": "prefer_ipv4" +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/tun_singbox_inbound b/v2rayN/ServiceLib/Sample/tun_singbox_inbound new file mode 100644 index 00000000..db5b0b32 --- /dev/null +++ b/v2rayN/ServiceLib/Sample/tun_singbox_inbound @@ -0,0 +1,14 @@ +{ + "type": "tun", + "tag": "tun-in", + "interface_name": "singbox_tun", + "address": [ + "172.18.0.1/30", + "fdfe:dcba:9876::1/126" + ], + "mtu": 9000, + "auto_route": true, + "strict_route": false, + "stack": "system", + "sniff": true +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/tun_singbox_rules b/v2rayN/ServiceLib/Sample/tun_singbox_rules new file mode 100644 index 00000000..a4276134 --- /dev/null +++ b/v2rayN/ServiceLib/Sample/tun_singbox_rules @@ -0,0 +1,20 @@ +[ + { + "network": "udp", + "port": [ + 135, + 137, + 138, + 139, + 5353 + ], + "action": "reject" + }, + { + "ip_cidr": [ + "224.0.0.0/3", + "ff00::/8" + ], + "action": "reject" + } +] \ No newline at end of file diff --git a/v2rayN/ServiceLib/ServiceLib.csproj b/v2rayN/ServiceLib/ServiceLib.csproj new file mode 100644 index 00000000..ecbab780 --- /dev/null +++ b/v2rayN/ServiceLib/ServiceLib.csproj @@ -0,0 +1,81 @@ + + + + Library + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ResUI.resx + True + True + + + Designer + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + + + Designer + ResUI.Designer.cs + PublicResXFileCodeGenerator + + + Designer + PublicResXFileCodeGenerator + + + Designer + PublicResXFileCodeGenerator + + + Designer + PublicResXFileCodeGenerator + + + + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs new file mode 100644 index 00000000..e102f17d --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs @@ -0,0 +1,261 @@ +namespace ServiceLib.Services.CoreConfig; + +/// +/// Core configuration file processing class +/// +public class CoreConfigClashService +{ + private Config _config; + private static readonly string _tag = "CoreConfigClashService"; + + public CoreConfigClashService(Config config) + { + _config = config; + } + + public async Task GenerateClientCustomConfig(ProfileItem node, string? fileName) + { + var ret = new RetResult(); + if (node == null || fileName is null) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + + ret.Msg = ResUI.InitialConfiguration; + + try + { + if (node == null) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + + var addressFileName = node.Address; + if (addressFileName.IsNullOrEmpty()) + { + ret.Msg = ResUI.FailedGetDefaultConfiguration; + return ret; + } + if (!File.Exists(addressFileName)) + { + addressFileName = Path.Combine(Utils.GetConfigPath(), addressFileName); + } + if (!File.Exists(addressFileName)) + { + ret.Msg = ResUI.FailedReadConfiguration + "1"; + return ret; + } + + var tagYamlStr1 = "!"; + var tagYamlStr2 = "__strn__"; + var tagYamlStr3 = "!!str"; + var txtFile = File.ReadAllText(addressFileName); + txtFile = txtFile.Replace(tagYamlStr1, tagYamlStr2); + + //YAML anchors + if (txtFile.Contains("<<:") && txtFile.Contains('*') && txtFile.Contains('&')) + { + txtFile = YamlUtils.PreprocessYaml(txtFile); + } + + var fileContent = YamlUtils.FromYaml>(txtFile); + if (fileContent == null) + { + ret.Msg = ResUI.FailedConversionConfiguration; + return ret; + } + + //mixed-port + fileContent["mixed-port"] = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); + //log-level + fileContent["log-level"] = GetLogLevel(_config.CoreBasicItem.Loglevel); + + //external-controller + fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}"; + //allow-lan + if (_config.Inbound.First().AllowLANConn) + { + fileContent["allow-lan"] = "true"; + fileContent["bind-address"] = "*"; + } + else + { + fileContent["allow-lan"] = "false"; + } + + //ipv6 + fileContent["ipv6"] = _config.ClashUIItem.EnableIPv6; + + //mode + if (!fileContent.ContainsKey("mode")) + { + fileContent["mode"] = ERuleMode.Rule.ToString().ToLower(); + } + else + { + if (_config.ClashUIItem.RuleMode != ERuleMode.Unchanged) + { + fileContent["mode"] = _config.ClashUIItem.RuleMode.ToString().ToLower(); + } + } + + //enable tun mode + if (_config.TunModeItem.EnableTun) + { + var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml); + if (tun.IsNotEmpty()) + { + var tunContent = YamlUtils.FromYaml>(tun); + if (tunContent != null) + { + fileContent["tun"] = tunContent["tun"]; + } + } + } + + //Mixin + try + { + await MixinContent(fileContent, node); + } + catch (Exception ex) + { + Logging.SaveLog($"{_tag}-Mixin", ex); + } + + var txtFileNew = YamlUtils.ToYaml(fileContent).Replace(tagYamlStr2, tagYamlStr3); + await File.WriteAllTextAsync(fileName, txtFileNew); + //check again + if (!File.Exists(fileName)) + { + ret.Msg = ResUI.FailedReadConfiguration + "2"; + return ret; + } + + ClashApiManager.Instance.ProfileContent = fileContent; + + ret.Msg = string.Format(ResUI.SuccessfulConfiguration, $"{node.GetSummary()}"); + ret.Success = true; + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + + private async Task MixinContent(Dictionary fileContent, ProfileItem node) + { + if (!_config.ClashUIItem.EnableMixinContent) + { + return; + } + + var path = Utils.GetConfigPath(Global.ClashMixinConfigFileName); + if (!File.Exists(path)) + { + var mixin = EmbedUtils.GetEmbedText(Global.ClashMixinYaml); + await File.AppendAllTextAsync(path, mixin); + } + + var txtFile = await File.ReadAllTextAsync(Utils.GetConfigPath(Global.ClashMixinConfigFileName)); + + var mixinContent = YamlUtils.FromYaml>(txtFile); + if (mixinContent == null) + { + return; + } + foreach (var item in mixinContent) + { + if (!_config.TunModeItem.EnableTun && item.Key == "tun") + { + continue; + } + + if (item.Key.StartsWith("prepend-") + || item.Key.StartsWith("append-") + || item.Key.StartsWith("removed-")) + { + ModifyContentMerge(fileContent, item.Key, item.Value); + } + else + { + fileContent[item.Key] = item.Value; + } + } + return; + } + + private void ModifyContentMerge(Dictionary fileContent, string key, object value) + { + var blPrepend = false; + var blRemoved = false; + if (key.StartsWith("prepend-")) + { + blPrepend = true; + key = key.Replace("prepend-", ""); + } + else if (key.StartsWith("append-")) + { + blPrepend = false; + key = key.Replace("append-", ""); + } + else if (key.StartsWith("removed-")) + { + blRemoved = true; + key = key.Replace("removed-", ""); + } + else + { + return; + } + + if (!blRemoved && !fileContent.ContainsKey(key)) + { + fileContent.Add(key, value); + return; + } + var lstOri = (List)fileContent[key]; + var lstValue = (List)value; + + if (blRemoved) + { + foreach (var item in lstValue) + { + lstOri.RemoveAll(t => t.ToString().StartsWith(item.ToString())); + } + return; + } + + if (blPrepend) + { + lstValue.Reverse(); + lstValue.ForEach(item => lstOri.Insert(0, item)); + } + else + { + lstValue.ForEach(item => lstOri.Add(item)); + } + } + + private string GetLogLevel(string level) + { + if (level == "none") + { + return "silent"; + } + else + { + return level; + } + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs new file mode 100644 index 00000000..4e24d8e2 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs @@ -0,0 +1,522 @@ +using System.Net; +using System.Net.NetworkInformation; + +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService(Config config) +{ + private readonly Config _config = config; + private static readonly string _tag = "CoreConfigSingboxService"; + + #region public gen function + + public async Task GenerateClientConfigContent(ProfileItem node) + { + var ret = new RetResult(); + try + { + if (node == null + || node.Port <= 0) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + if (node.GetNetwork() is nameof(ETransport.kcp) or nameof(ETransport.xhttp)) + { + ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}"; + return ret; + } + + ret.Msg = ResUI.InitialConfiguration; + + var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient); + if (result.IsNullOrEmpty()) + { + ret.Msg = ResUI.FailedGetDefaultConfiguration; + return ret; + } + + var singboxConfig = JsonUtils.Deserialize(result); + if (singboxConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + await GenLog(singboxConfig); + + await GenInbounds(singboxConfig); + + if (node.ConfigType == EConfigType.WireGuard) + { + singboxConfig.outbounds.RemoveAt(0); + var endpoints = new Endpoints4Sbox(); + await GenEndpoint(node, endpoints); + endpoints.tag = Global.ProxyTag; + singboxConfig.endpoints = new() { endpoints }; + } + else + { + await GenOutbound(node, singboxConfig.outbounds.First()); + } + + await GenMoreOutbounds(node, singboxConfig); + + await GenRouting(singboxConfig); + + await GenDns(node, singboxConfig); + + await GenExperimental(singboxConfig); + + await ConvertGeo2Ruleset(singboxConfig); + + ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); + ret.Success = true; + + ret.Data = await ApplyFullConfigTemplate(singboxConfig); + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + + public async Task GenerateClientSpeedtestConfig(List selecteds) + { + var ret = new RetResult(); + try + { + if (_config == null) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + + ret.Msg = ResUI.InitialConfiguration; + + var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient); + var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); + if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty()) + { + ret.Msg = ResUI.FailedGetDefaultConfiguration; + return ret; + } + + var singboxConfig = JsonUtils.Deserialize(result); + if (singboxConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + List lstIpEndPoints = new(); + List lstTcpConns = new(); + try + { + lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners()); + lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners()); + lstTcpConns.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections()); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + + await GenLog(singboxConfig); + //GenDns(new(), singboxConfig); + singboxConfig.inbounds.Clear(); + singboxConfig.outbounds.RemoveAt(0); + + var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest); + + foreach (var it in selecteds) + { + if (!Global.SingboxSupportConfigType.Contains(it.ConfigType)) + { + continue; + } + if (it.Port <= 0) + { + continue; + } + var item = await AppManager.Instance.GetProfileItem(it.IndexId); + if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) + { + if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) + { + continue; + } + } + + //find unused port + var port = initPort; + for (var k = initPort; k < Global.MaxPort; k++) + { + if (lstIpEndPoints?.FindIndex(_it => _it.Port == k) >= 0) + { + continue; + } + if (lstTcpConns?.FindIndex(_it => _it.LocalEndPoint.Port == k) >= 0) + { + continue; + } + //found + port = k; + initPort = port + 1; + break; + } + + //Port In Used + if (lstIpEndPoints?.FindIndex(_it => _it.Port == port) >= 0) + { + continue; + } + it.Port = port; + it.AllowTest = true; + + //inbound + Inbound4Sbox inbound = new() + { + listen = Global.Loopback, + listen_port = port, + type = EInboundProtocol.mixed.ToString(), + }; + inbound.tag = inbound.type + inbound.listen_port.ToString(); + singboxConfig.inbounds.Add(inbound); + + //outbound + if (item is null) + { + continue; + } + if (item.ConfigType == EConfigType.Shadowsocks + && !Global.SsSecuritiesInSingbox.Contains(item.Security)) + { + continue; + } + if (item.ConfigType == EConfigType.VLESS + && !Global.Flows.Contains(item.Flow)) + { + continue; + } + if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan + && item.StreamSecurity == Global.StreamSecurityReality + && item.PublicKey.IsNullOrEmpty()) + { + continue; + } + + var server = await GenServer(item); + if (server is null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + var tag = Global.ProxyTag + inbound.listen_port.ToString(); + server.tag = tag; + if (server is Endpoints4Sbox endpoint) + { + singboxConfig.endpoints ??= new(); + singboxConfig.endpoints.Add(endpoint); + } + else if (server is Outbound4Sbox outbound) + { + singboxConfig.outbounds.Add(outbound); + } + + //rule + Rule4Sbox rule = new() + { + inbound = new List { inbound.tag }, + outbound = tag + }; + singboxConfig.route.rules.Add(rule); + } + + var rawDNSItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); + if (rawDNSItem != null && rawDNSItem.Enabled == true) + { + await GenDnsDomainsCompatible(singboxConfig, rawDNSItem); + } + else + { + await GenDnsDomains(singboxConfig, _config.SimpleDNSItem); + } + singboxConfig.route.default_domain_resolver = new() + { + server = Global.SingboxFinalResolverTag + }; + + ret.Success = true; + ret.Data = JsonUtils.Serialize(singboxConfig); + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + + public async Task GenerateClientSpeedtestConfig(ProfileItem node, int port) + { + var ret = new RetResult(); + try + { + if (node is not { Port: > 0 }) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + if (node.GetNetwork() is nameof(ETransport.kcp) or nameof(ETransport.xhttp)) + { + ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}"; + return ret; + } + + ret.Msg = ResUI.InitialConfiguration; + + var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient); + if (result.IsNullOrEmpty()) + { + ret.Msg = ResUI.FailedGetDefaultConfiguration; + return ret; + } + + var singboxConfig = JsonUtils.Deserialize(result); + if (singboxConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + await GenLog(singboxConfig); + if (node.ConfigType == EConfigType.WireGuard) + { + singboxConfig.outbounds.RemoveAt(0); + var endpoints = new Endpoints4Sbox(); + await GenEndpoint(node, endpoints); + endpoints.tag = Global.ProxyTag; + singboxConfig.endpoints = new() { endpoints }; + } + else + { + await GenOutbound(node, singboxConfig.outbounds.First()); + } + await GenMoreOutbounds(node, singboxConfig); + var item = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); + if (item != null && item.Enabled == true) + { + await GenDnsDomainsCompatible(singboxConfig, item); + } + else + { + await GenDnsDomains(singboxConfig, _config.SimpleDNSItem); + } + singboxConfig.route.default_domain_resolver = new() + { + server = Global.SingboxFinalResolverTag + }; + + singboxConfig.route.rules.Clear(); + singboxConfig.inbounds.Clear(); + singboxConfig.inbounds.Add(new() + { + tag = $"{EInboundProtocol.mixed}{port}", + listen = Global.Loopback, + listen_port = port, + type = EInboundProtocol.mixed.ToString(), + }); + + ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); + ret.Success = true; + ret.Data = JsonUtils.Serialize(singboxConfig); + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + + public async Task GenerateClientMultipleLoadConfig(List selecteds) + { + var ret = new RetResult(); + try + { + if (_config == null) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + + ret.Msg = ResUI.InitialConfiguration; + + var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient); + var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); + if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty()) + { + ret.Msg = ResUI.FailedGetDefaultConfiguration; + return ret; + } + + var singboxConfig = JsonUtils.Deserialize(result); + if (singboxConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + await GenLog(singboxConfig); + await GenInbounds(singboxConfig); + await GenRouting(singboxConfig); + await GenExperimental(singboxConfig); + singboxConfig.outbounds.RemoveAt(0); + + var proxyProfiles = new List(); + foreach (var it in selecteds) + { + if (!Global.SingboxSupportConfigType.Contains(it.ConfigType)) + { + continue; + } + if (it.Port <= 0) + { + continue; + } + var item = await AppManager.Instance.GetProfileItem(it.IndexId); + if (item is null) + { + continue; + } + if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) + { + if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) + { + continue; + } + } + if (item.ConfigType == EConfigType.Shadowsocks + && !Global.SsSecuritiesInSingbox.Contains(item.Security)) + { + continue; + } + if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow)) + { + continue; + } + + //outbound + proxyProfiles.Add(item); + } + if (proxyProfiles.Count <= 0) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + await GenOutboundsList(proxyProfiles, singboxConfig); + + await GenDns(null, singboxConfig); + await ConvertGeo2Ruleset(singboxConfig); + + ret.Success = true; + + ret.Data = await ApplyFullConfigTemplate(singboxConfig); + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + + public async Task GenerateClientCustomConfig(ProfileItem node, string? fileName) + { + var ret = new RetResult(); + if (node == null || fileName is null) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + + ret.Msg = ResUI.InitialConfiguration; + + try + { + if (node == null) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + + var addressFileName = node.Address; + if (addressFileName.IsNullOrEmpty()) + { + ret.Msg = ResUI.FailedGetDefaultConfiguration; + return ret; + } + if (!File.Exists(addressFileName)) + { + addressFileName = Path.Combine(Utils.GetConfigPath(), addressFileName); + } + if (!File.Exists(addressFileName)) + { + ret.Msg = ResUI.FailedReadConfiguration + "1"; + return ret; + } + + if (node.Address == Global.CoreMultipleLoadConfigFileName) + { + var txtFile = File.ReadAllText(addressFileName); + var singboxConfig = JsonUtils.Deserialize(txtFile); + if (singboxConfig == null) + { + File.Copy(addressFileName, fileName); + } + else + { + await GenInbounds(singboxConfig); + await GenExperimental(singboxConfig); + + var content = JsonUtils.Serialize(singboxConfig, true); + await File.WriteAllTextAsync(fileName, content); + } + } + else + { + File.Copy(addressFileName, fileName); + } + + //check again + if (!File.Exists(fileName)) + { + ret.Msg = ResUI.FailedReadConfiguration + "2"; + return ret; + } + + ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); + ret.Success = true; + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + + #endregion public gen function +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs new file mode 100644 index 00000000..c6bec22b --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Nodes; + +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task ApplyFullConfigTemplate(SingboxConfig singboxConfig) + { + var fullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box); + if (fullConfigTemplate == null || !fullConfigTemplate.Enabled) + { + return JsonUtils.Serialize(singboxConfig); + } + + var fullConfigTemplateItem = _config.TunModeItem.EnableTun ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config; + if (fullConfigTemplateItem.IsNullOrEmpty()) + { + return JsonUtils.Serialize(singboxConfig); + } + + var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem); + if (fullConfigTemplateNode == null) + { + return JsonUtils.Serialize(singboxConfig); + } + + // Process outbounds + var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray(); + foreach (var outbound in singboxConfig.outbounds) + { + if (outbound.type.ToLower() is "direct" or "block") + { + if (fullConfigTemplate.AddProxyOnly == true) + { + continue; + } + } + else if (outbound.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty() && !Utils.IsPrivateNetwork(outbound.server ?? string.Empty)) + { + outbound.detour = fullConfigTemplate.ProxyDetour; + } + customOutboundsNode.Add(JsonUtils.DeepCopy(outbound)); + } + fullConfigTemplateNode["outbounds"] = customOutboundsNode; + + // Process endpoints + if (singboxConfig.endpoints != null && singboxConfig.endpoints.Count > 0) + { + var customEndpointsNode = fullConfigTemplateNode["endpoints"] is JsonArray endpoints ? endpoints : new JsonArray(); + foreach (var endpoint in singboxConfig.endpoints) + { + if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty()) + { + endpoint.detour = fullConfigTemplate.ProxyDetour; + } + customEndpointsNode.Add(JsonUtils.DeepCopy(endpoint)); + } + fullConfigTemplateNode["endpoints"] = customEndpointsNode; + } + + return await Task.FromResult(JsonUtils.Serialize(fullConfigTemplateNode)); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs new file mode 100644 index 00000000..2f0f8015 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -0,0 +1,496 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task GenDns(ProfileItem? node, SingboxConfig singboxConfig) + { + try + { + var item = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); + if (item != null && item.Enabled == true) + { + return await GenDnsCompatible(node, singboxConfig); + } + + var simpleDNSItem = _config.SimpleDNSItem; + await GenDnsServers(singboxConfig, simpleDNSItem); + await GenDnsRules(singboxConfig, simpleDNSItem); + + singboxConfig.dns ??= new Dns4Sbox(); + singboxConfig.dns.independent_cache = true; + + // final dns + var routing = await ConfigHandler.GetDefaultRouting(_config); + var useDirectDns = false; + if (routing != null) + { + var rules = JsonUtils.Deserialize>(routing.RuleSet) ?? []; + + useDirectDns = rules?.LastOrDefault() is { } lastRule && + lastRule.OutboundTag == Global.DirectTag && + (lastRule.Port == "0-65535" || + lastRule.Network == "tcp,udp" || + lastRule.Ip?.Contains("0.0.0.0/0") == true); + } + singboxConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag; + + // Tun2SocksAddress + if (node != null && Utils.IsDomain(node.Address)) + { + singboxConfig.dns.rules ??= new List(); + singboxConfig.dns.rules.Insert(0, new Rule4Sbox + { + server = Global.SingboxOutboundResolverTag, + domain = [node.Address], + }); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenDnsServers(SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem) + { + var finalDns = await GenDnsDomains(singboxConfig, simpleDNSItem); + + var directDns = ParseDnsAddress(simpleDNSItem.DirectDNS); + directDns.tag = Global.SingboxDirectDNSTag; + directDns.domain_resolver = Global.SingboxFinalResolverTag; + + var remoteDns = ParseDnsAddress(simpleDNSItem.RemoteDNS); + remoteDns.tag = Global.SingboxRemoteDNSTag; + remoteDns.detour = Global.ProxyTag; + remoteDns.domain_resolver = Global.SingboxFinalResolverTag; + + var resolverDns = ParseDnsAddress(simpleDNSItem.SingboxOutboundsResolveDNS); + resolverDns.tag = Global.SingboxOutboundResolverTag; + resolverDns.domain_resolver = Global.SingboxFinalResolverTag; + + var hostsDns = new Server4Sbox + { + tag = Global.SingboxHostsDNSTag, + type = "hosts", + predefined = new(), + }; + if (simpleDNSItem.AddCommonHosts == true) + { + hostsDns.predefined = Global.PredefinedHosts; + } + + if (simpleDNSItem.UseSystemHosts == true) + { + var systemHosts = Utils.GetSystemHosts(); + if (systemHosts != null && systemHosts.Count > 0) + { + foreach (var host in systemHosts) + { + hostsDns.predefined.TryAdd(host.Key, new List { host.Value }); + } + } + } + + if (!simpleDNSItem.Hosts.IsNullOrEmpty()) + { + var userHostsMap = simpleDNSItem.Hosts + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .Where(line => !string.IsNullOrWhiteSpace(line) && line.Contains(' ')) + .Select(line => line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries)) + .Where(parts => parts.Length >= 2) + .GroupBy(parts => parts[0]) + .ToDictionary( + group => group.Key, + group => group.SelectMany(parts => parts.Skip(1)).ToList() + ); + + foreach (var kvp in userHostsMap) + { + hostsDns.predefined[kvp.Key] = kvp.Value; + } + } + + foreach (var host in hostsDns.predefined) + { + if (finalDns.server == host.Key) + { + finalDns.domain_resolver = Global.SingboxHostsDNSTag; + } + if (remoteDns.server == host.Key) + { + remoteDns.domain_resolver = Global.SingboxHostsDNSTag; + } + if (resolverDns.server == host.Key) + { + resolverDns.domain_resolver = Global.SingboxHostsDNSTag; + } + if (directDns.server == host.Key) + { + directDns.domain_resolver = Global.SingboxHostsDNSTag; + } + } + + singboxConfig.dns ??= new Dns4Sbox(); + singboxConfig.dns.servers ??= new List(); + singboxConfig.dns.servers.Add(remoteDns); + singboxConfig.dns.servers.Add(directDns); + singboxConfig.dns.servers.Add(resolverDns); + singboxConfig.dns.servers.Add(hostsDns); + + // fake ip + if (simpleDNSItem.FakeIP == true) + { + var fakeip = new Server4Sbox + { + tag = Global.SingboxFakeDNSTag, + type = "fakeip", + inet4_range = "198.18.0.0/15", + inet6_range = "fc00::/18", + }; + singboxConfig.dns.servers.Add(fakeip); + } + + return await Task.FromResult(0); + } + + private async Task GenDnsDomains(SingboxConfig singboxConfig, SimpleDNSItem? simpleDNSItem) + { + var finalDns = ParseDnsAddress(simpleDNSItem.SingboxFinalResolveDNS); + finalDns.tag = Global.SingboxFinalResolverTag; + singboxConfig.dns ??= new Dns4Sbox(); + singboxConfig.dns.servers ??= new List(); + singboxConfig.dns.servers.Add(finalDns); + return await Task.FromResult(finalDns); + } + + private async Task GenDnsRules(SingboxConfig singboxConfig, SimpleDNSItem simpleDNSItem) + { + singboxConfig.dns ??= new Dns4Sbox(); + singboxConfig.dns.rules ??= new List(); + + singboxConfig.dns.rules.AddRange(new[] + { + new Rule4Sbox { ip_accept_any = true, server = Global.SingboxHostsDNSTag }, + new Rule4Sbox + { + server = Global.SingboxRemoteDNSTag, + strategy = simpleDNSItem.SingboxStrategy4Proxy.IsNullOrEmpty() ? null : simpleDNSItem.SingboxStrategy4Proxy, + clash_mode = ERuleMode.Global.ToString() + }, + new Rule4Sbox + { + server = Global.SingboxDirectDNSTag, + strategy = simpleDNSItem.SingboxStrategy4Direct.IsNullOrEmpty() ? null : simpleDNSItem.SingboxStrategy4Direct, + clash_mode = ERuleMode.Direct.ToString() + } + }); + + if (simpleDNSItem.BlockBindingQuery == true) + { + singboxConfig.dns.rules.Add(new() + { + query_type = new List { 64, 65 }, + action = "predefined", + rcode = "NOTIMP" + }); + } + + var routing = await ConfigHandler.GetDefaultRouting(_config); + if (routing == null) + return 0; + + var rules = JsonUtils.Deserialize>(routing.RuleSet) ?? []; + var expectedIPCidr = new List(); + var expectedIPsRegions = new List(); + var regionNames = new HashSet(); + + if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs)) + { + var ipItems = simpleDNSItem.DirectExpectedIPs + .Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToList(); + + foreach (var ip in ipItems) + { + if (ip.StartsWith("geoip:", StringComparison.OrdinalIgnoreCase)) + { + var region = ip["geoip:".Length..]; + if (!string.IsNullOrEmpty(region)) + { + expectedIPsRegions.Add(region); + regionNames.Add(region); + regionNames.Add($"geolocation-{region}"); + regionNames.Add($"tld-{region}"); + } + } + else + { + expectedIPCidr.Add(ip); + } + } + } + + foreach (var item in rules) + { + if (!item.Enabled || item.Domain is null || item.Domain.Count == 0) + { + continue; + } + + var rule = new Rule4Sbox(); + var validDomains = item.Domain.Count(it => ParseV2Domain(it, rule)); + if (validDomains <= 0) + { + continue; + } + + if (item.OutboundTag == Global.DirectTag) + { + rule.server = Global.SingboxDirectDNSTag; + rule.strategy = string.IsNullOrEmpty(simpleDNSItem.SingboxStrategy4Direct) ? null : simpleDNSItem.SingboxStrategy4Direct; + + if (expectedIPsRegions.Count > 0 && rule.geosite?.Count > 0) + { + var geositeSet = new HashSet(rule.geosite); + if (regionNames.Intersect(geositeSet).Any()) + { + if (expectedIPsRegions.Count > 0) + { + rule.geoip = expectedIPsRegions; + } + if (expectedIPCidr.Count > 0) + { + rule.ip_cidr = expectedIPCidr; + } + } + } + } + else if (item.OutboundTag == Global.BlockTag) + { + rule.action = "predefined"; + rule.rcode = "NXDOMAIN"; + } + else + { + if (simpleDNSItem.FakeIP == true) + { + var rule4Fake = JsonUtils.DeepCopy(rule); + rule4Fake.server = Global.SingboxFakeDNSTag; + singboxConfig.dns.rules.Add(rule4Fake); + } + rule.server = Global.SingboxRemoteDNSTag; + rule.strategy = string.IsNullOrEmpty(simpleDNSItem.SingboxStrategy4Proxy) ? null : simpleDNSItem.SingboxStrategy4Proxy; + } + + singboxConfig.dns.rules.Add(rule); + } + + return 0; + } + + private async Task GenDnsCompatible(ProfileItem? node, SingboxConfig singboxConfig) + { + try + { + var item = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); + var strDNS = string.Empty; + if (_config.TunModeItem.EnableTun) + { + strDNS = string.IsNullOrEmpty(item?.TunDNS) ? EmbedUtils.GetEmbedText(Global.TunSingboxDNSFileName) : item?.TunDNS; + } + else + { + strDNS = string.IsNullOrEmpty(item?.NormalDNS) ? EmbedUtils.GetEmbedText(Global.DNSSingboxNormalFileName) : item?.NormalDNS; + } + + var dns4Sbox = JsonUtils.Deserialize(strDNS); + if (dns4Sbox is null) + { + return 0; + } + singboxConfig.dns = dns4Sbox; + + if (dns4Sbox.servers != null && dns4Sbox.servers.Count > 0 && dns4Sbox.servers.First().address.IsNullOrEmpty()) + { + await GenDnsDomainsCompatible(singboxConfig, item); + } + else + { + await GenDnsDomainsLegacyCompatible(singboxConfig, item); + } + + // Tun2SocksAddress + if (node != null && Utils.IsDomain(node.Address)) + { + singboxConfig.dns.rules ??= new List(); + singboxConfig.dns.rules.Insert(0, new Rule4Sbox + { + server = Global.SingboxFinalResolverTag, + domain = [node.Address], + }); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenDnsDomainsCompatible(SingboxConfig singboxConfig, DNSItem? dNSItem) + { + var dns4Sbox = singboxConfig.dns ?? new(); + dns4Sbox.servers ??= []; + dns4Sbox.rules ??= []; + + var tag = Global.SingboxFinalResolverTag; + var localDnsAddress = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dNSItem?.DomainDNSAddress; + + var localDnsServer = ParseDnsAddress(localDnsAddress); + localDnsServer.tag = tag; + + dns4Sbox.servers.Add(localDnsServer); + + singboxConfig.dns = dns4Sbox; + return await Task.FromResult(0); + } + + private async Task GenDnsDomainsLegacyCompatible(SingboxConfig singboxConfig, DNSItem? dNSItem) + { + var dns4Sbox = singboxConfig.dns ?? new(); + dns4Sbox.servers ??= []; + dns4Sbox.rules ??= []; + + var tag = Global.SingboxFinalResolverTag; + dns4Sbox.servers.Add(new() + { + tag = tag, + address = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dNSItem?.DomainDNSAddress, + detour = Global.DirectTag, + strategy = string.IsNullOrEmpty(dNSItem?.DomainStrategy4Freedom) ? null : dNSItem?.DomainStrategy4Freedom, + }); + dns4Sbox.rules.Insert(0, new() + { + server = tag, + clash_mode = ERuleMode.Direct.ToString() + }); + dns4Sbox.rules.Insert(0, new() + { + server = dns4Sbox.servers.Where(t => t.detour == Global.ProxyTag).Select(t => t.tag).FirstOrDefault() ?? "remote", + clash_mode = ERuleMode.Global.ToString() + }); + + var lstDomain = singboxConfig.outbounds + .Where(t => t.server.IsNotEmpty() && Utils.IsDomain(t.server)) + .Select(t => t.server) + .Distinct() + .ToList(); + if (lstDomain != null && lstDomain.Count > 0) + { + dns4Sbox.rules.Insert(0, new() + { + server = tag, + domain = lstDomain + }); + } + + singboxConfig.dns = dns4Sbox; + return await Task.FromResult(0); + } + + private static Server4Sbox? ParseDnsAddress(string address) + { + var addressFirst = address?.Split(address.Contains(',') ? ',' : ';').FirstOrDefault()?.Trim(); + if (string.IsNullOrEmpty(addressFirst)) + { + return null; + } + + var server = new Server4Sbox(); + + if (addressFirst is "local" or "localhost") + { + server.type = "local"; + return server; + } + + if (addressFirst.StartsWith("dhcp://", StringComparison.OrdinalIgnoreCase)) + { + var interface_name = addressFirst.Substring(7); + server.type = "dhcp"; + server.Interface = interface_name == "auto" ? null : interface_name; + return server; + } + + if (!addressFirst.Contains("://")) + { + // udp dns + server.type = "udp"; + server.server = addressFirst; + return server; + } + + try + { + var protocolEndIndex = addressFirst.IndexOf("://", StringComparison.Ordinal); + server.type = addressFirst.Substring(0, protocolEndIndex).ToLower(); + + var uri = new Uri(addressFirst); + server.server = uri.Host; + + if (!uri.IsDefaultPort) + { + server.server_port = uri.Port; + } + + if ((server.type == "https" || server.type == "h3") && !string.IsNullOrEmpty(uri.AbsolutePath) && uri.AbsolutePath != "/") + { + server.path = uri.AbsolutePath; + } + } + catch (UriFormatException) + { + var protocolEndIndex = addressFirst.IndexOf("://", StringComparison.Ordinal); + if (protocolEndIndex > 0) + { + server.type = addressFirst.Substring(0, protocolEndIndex).ToLower(); + var remaining = addressFirst.Substring(protocolEndIndex + 3); + + var portIndex = remaining.IndexOf(':'); + var pathIndex = remaining.IndexOf('/'); + + if (portIndex > 0) + { + server.server = remaining.Substring(0, portIndex); + var portPart = pathIndex > portIndex + ? remaining.Substring(portIndex + 1, pathIndex - portIndex - 1) + : remaining.Substring(portIndex + 1); + + if (int.TryParse(portPart, out var parsedPort)) + { + server.server_port = parsedPort; + } + } + else if (pathIndex > 0) + { + server.server = remaining.Substring(0, pathIndex); + } + else + { + server.server = remaining; + } + + if (pathIndex > 0 && (server.type == "https" || server.type == "h3")) + { + server.path = remaining.Substring(pathIndex); + } + } + } + + return server; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs new file mode 100644 index 00000000..91f76ad9 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs @@ -0,0 +1,92 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task GenInbounds(SingboxConfig singboxConfig) + { + try + { + var listen = "0.0.0.0"; + singboxConfig.inbounds = []; + + if (!_config.TunModeItem.EnableTun + || (_config.TunModeItem.EnableTun && _config.TunModeItem.EnableExInbound && _config.RunningCoreType == ECoreType.sing_box)) + { + var inbound = new Inbound4Sbox() + { + type = EInboundProtocol.mixed.ToString(), + tag = EInboundProtocol.socks.ToString(), + listen = Global.Loopback, + }; + singboxConfig.inbounds.Add(inbound); + + inbound.listen_port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); + + if (_config.Inbound.First().SecondLocalPortEnabled) + { + var inbound2 = GetInbound(inbound, EInboundProtocol.socks2, true); + singboxConfig.inbounds.Add(inbound2); + } + + if (_config.Inbound.First().AllowLANConn) + { + if (_config.Inbound.First().NewPort4LAN) + { + var inbound3 = GetInbound(inbound, EInboundProtocol.socks3, true); + inbound3.listen = listen; + singboxConfig.inbounds.Add(inbound3); + + //auth + if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty()) + { + inbound3.users = new() { new() { username = _config.Inbound.First().User, password = _config.Inbound.First().Pass } }; + } + } + else + { + inbound.listen = listen; + } + } + } + + if (_config.TunModeItem.EnableTun) + { + if (_config.TunModeItem.Mtu <= 0) + { + _config.TunModeItem.Mtu = Global.TunMtus.First(); + } + if (_config.TunModeItem.Stack.IsNullOrEmpty()) + { + _config.TunModeItem.Stack = Global.TunStacks.First(); + } + + var tunInbound = JsonUtils.Deserialize(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { }; + tunInbound.interface_name = Utils.IsOSX() ? $"utun{new Random().Next(99)}" : "singbox_tun"; + tunInbound.mtu = _config.TunModeItem.Mtu; + tunInbound.auto_route = _config.TunModeItem.AutoRoute; + tunInbound.strict_route = _config.TunModeItem.StrictRoute; + tunInbound.stack = _config.TunModeItem.Stack; + if (_config.TunModeItem.EnableIPv6Address == false) + { + tunInbound.address = ["172.18.0.1/30"]; + } + + singboxConfig.inbounds.Add(tunInbound); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + + private Inbound4Sbox GetInbound(Inbound4Sbox inItem, EInboundProtocol protocol, bool bSocks) + { + var inbound = JsonUtils.DeepCopy(inItem); + inbound.tag = protocol.ToString(); + inbound.listen_port = inItem.listen_port + (int)protocol; + inbound.type = EInboundProtocol.mixed.ToString(); + return inbound; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxLogService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxLogService.cs new file mode 100644 index 00000000..59e65471 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxLogService.cs @@ -0,0 +1,40 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task GenLog(SingboxConfig singboxConfig) + { + try + { + switch (_config.CoreBasicItem.Loglevel) + { + case "debug": + case "info": + case "error": + singboxConfig.log.level = _config.CoreBasicItem.Loglevel; + break; + + case "warning": + singboxConfig.log.level = "warn"; + break; + + default: + break; + } + if (_config.CoreBasicItem.Loglevel == Global.None) + { + singboxConfig.log.disabled = true; + } + if (_config.CoreBasicItem.LogEnabled) + { + var dtNow = DateTime.Now; + singboxConfig.log.output = Utils.GetLogPath($"sbox_{dtNow:yyyy-MM-dd}.txt"); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs new file mode 100644 index 00000000..3f03b93c --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs @@ -0,0 +1,577 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task GenOutbound(ProfileItem node, Outbound4Sbox outbound) + { + try + { + outbound.server = node.Address; + outbound.server_port = node.Port; + outbound.type = Global.ProtocolTypes[node.ConfigType]; + + switch (node.ConfigType) + { + case EConfigType.VMess: + { + outbound.uuid = node.Id; + outbound.alter_id = node.AlterId; + if (Global.VmessSecurities.Contains(node.Security)) + { + outbound.security = node.Security; + } + else + { + outbound.security = Global.DefaultSecurity; + } + + await GenOutboundMux(node, outbound); + break; + } + case EConfigType.Shadowsocks: + { + outbound.method = AppManager.Instance.GetShadowsocksSecurities(node).Contains(node.Security) ? node.Security : Global.None; + outbound.password = node.Id; + + await GenOutboundMux(node, outbound); + break; + } + case EConfigType.SOCKS: + { + outbound.version = "5"; + if (node.Security.IsNotEmpty() + && node.Id.IsNotEmpty()) + { + outbound.username = node.Security; + outbound.password = node.Id; + } + break; + } + case EConfigType.HTTP: + { + if (node.Security.IsNotEmpty() + && node.Id.IsNotEmpty()) + { + outbound.username = node.Security; + outbound.password = node.Id; + } + break; + } + case EConfigType.VLESS: + { + outbound.uuid = node.Id; + + outbound.packet_encoding = "xudp"; + + if (node.Flow.IsNullOrEmpty()) + { + await GenOutboundMux(node, outbound); + } + else + { + outbound.flow = node.Flow; + } + break; + } + case EConfigType.Trojan: + { + outbound.password = node.Id; + + await GenOutboundMux(node, outbound); + break; + } + case EConfigType.Hysteria2: + { + outbound.password = node.Id; + + if (node.Path.IsNotEmpty()) + { + outbound.obfs = new() + { + type = "salamander", + password = node.Path.TrimEx(), + }; + } + + outbound.up_mbps = _config.HysteriaItem.UpMbps > 0 ? _config.HysteriaItem.UpMbps : null; + outbound.down_mbps = _config.HysteriaItem.DownMbps > 0 ? _config.HysteriaItem.DownMbps : null; + if (node.Ports.IsNotEmpty() && (node.Ports.Contains(':') || node.Ports.Contains('-') || node.Ports.Contains(','))) + { + outbound.server_port = null; + outbound.server_ports = node.Ports.Split(',') + .Select(p => p.Trim()) + .Where(p => p.IsNotEmpty()) + .Select(p => + { + var port = p.Replace('-', ':'); + return port.Contains(':') ? port : $"{port}:{port}"; + }) + .ToList(); + outbound.hop_interval = _config.HysteriaItem.HopInterval > 0 ? $"{_config.HysteriaItem.HopInterval}s" : null; + } + + break; + } + case EConfigType.TUIC: + { + outbound.uuid = node.Id; + outbound.password = node.Security; + outbound.congestion_control = node.HeaderType; + break; + } + case EConfigType.Anytls: + { + outbound.password = node.Id; + break; + } + } + + await GenOutboundTls(node, outbound); + + await GenOutboundTransport(node, outbound); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenEndpoint(ProfileItem node, Endpoints4Sbox endpoint) + { + try + { + endpoint.address = Utils.String2List(node.RequestHost); + endpoint.type = Global.ProtocolTypes[node.ConfigType]; + + switch (node.ConfigType) + { + case EConfigType.WireGuard: + { + var peer = new Peer4Sbox + { + public_key = node.PublicKey, + reserved = Utils.String2List(node.Path)?.Select(int.Parse).ToList(), + address = node.Address, + port = node.Port, + // TODO default ["0.0.0.0/0", "::/0"] + allowed_ips = new() { "0.0.0.0/0", "::/0" }, + }; + endpoint.private_key = node.Id; + endpoint.mtu = node.ShortId.IsNullOrEmpty() ? Global.TunMtus.First() : node.ShortId.ToInt(); + endpoint.peers = new() { peer }; + break; + } + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + + private async Task GenServer(ProfileItem node) + { + try + { + var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); + if (node.ConfigType == EConfigType.WireGuard) + { + var endpoint = JsonUtils.Deserialize(txtOutbound); + await GenEndpoint(node, endpoint); + return endpoint; + } + else + { + var outbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(node, outbound); + return outbound; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(null); + } + + private async Task GenOutboundMux(ProfileItem node, Outbound4Sbox outbound) + { + try + { + var muxEnabled = node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled; + if (muxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty()) + { + var mux = new Multiplex4Sbox() + { + enabled = true, + protocol = _config.Mux4SboxItem.Protocol, + max_connections = _config.Mux4SboxItem.MaxConnections, + padding = _config.Mux4SboxItem.Padding, + }; + outbound.multiplex = mux; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + + private async Task GenOutboundTls(ProfileItem node, Outbound4Sbox outbound) + { + try + { + if (node.StreamSecurity == Global.StreamSecurityReality || node.StreamSecurity == Global.StreamSecurity) + { + var server_name = string.Empty; + if (node.Sni.IsNotEmpty()) + { + server_name = node.Sni; + } + else if (node.RequestHost.IsNotEmpty()) + { + server_name = Utils.String2List(node.RequestHost)?.First(); + } + var tls = new Tls4Sbox() + { + enabled = true, + record_fragment = _config.CoreBasicItem.EnableFragment, + server_name = server_name, + insecure = Utils.ToBool(node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : node.AllowInsecure), + alpn = node.GetAlpn(), + }; + if (node.Fingerprint.IsNotEmpty()) + { + tls.utls = new Utls4Sbox() + { + enabled = true, + fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint + }; + } + if (node.StreamSecurity == Global.StreamSecurityReality) + { + tls.reality = new Reality4Sbox() + { + enabled = true, + public_key = node.PublicKey, + short_id = node.ShortId + }; + tls.insecure = false; + } + outbound.tls = tls; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + + private async Task GenOutboundTransport(ProfileItem node, Outbound4Sbox outbound) + { + try + { + var transport = new Transport4Sbox(); + + switch (node.GetNetwork()) + { + case nameof(ETransport.h2): + transport.type = nameof(ETransport.http); + transport.host = node.RequestHost.IsNullOrEmpty() ? null : Utils.String2List(node.RequestHost); + transport.path = node.Path.IsNullOrEmpty() ? null : node.Path; + break; + + case nameof(ETransport.tcp): //http + if (node.HeaderType == Global.TcpHeaderHttp) + { + if (node.ConfigType == EConfigType.Shadowsocks) + { + outbound.plugin = "obfs-local"; + outbound.plugin_opts = $"obfs=http;obfs-host={node.RequestHost};"; + } + else + { + transport.type = nameof(ETransport.http); + transport.host = node.RequestHost.IsNullOrEmpty() ? null : Utils.String2List(node.RequestHost); + transport.path = node.Path.IsNullOrEmpty() ? null : node.Path; + } + } + break; + + case nameof(ETransport.ws): + transport.type = nameof(ETransport.ws); + transport.path = node.Path.IsNullOrEmpty() ? null : node.Path; + if (node.RequestHost.IsNotEmpty()) + { + transport.headers = new() + { + Host = node.RequestHost + }; + } + break; + + case nameof(ETransport.httpupgrade): + transport.type = nameof(ETransport.httpupgrade); + transport.path = node.Path.IsNullOrEmpty() ? null : node.Path; + transport.host = node.RequestHost.IsNullOrEmpty() ? null : node.RequestHost; + + break; + + case nameof(ETransport.quic): + transport.type = nameof(ETransport.quic); + break; + + case nameof(ETransport.grpc): + transport.type = nameof(ETransport.grpc); + transport.service_name = node.Path; + transport.idle_timeout = _config.GrpcItem.IdleTimeout?.ToString("##s"); + transport.ping_timeout = _config.GrpcItem.HealthCheckTimeout?.ToString("##s"); + transport.permit_without_stream = _config.GrpcItem.PermitWithoutStream; + break; + + default: + break; + } + if (transport.type != null) + { + outbound.transport = transport; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + + private async Task GenMoreOutbounds(ProfileItem node, SingboxConfig singboxConfig) + { + if (node.Subid.IsNullOrEmpty()) + { + return 0; + } + try + { + var subItem = await AppManager.Instance.GetSubItem(node.Subid); + if (subItem is null) + { + return 0; + } + + //current proxy + BaseServer4Sbox? outbound = singboxConfig.endpoints?.FirstOrDefault(t => t.tag == Global.ProxyTag, null); + outbound ??= singboxConfig.outbounds.First(); + + var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); + + //Previous proxy + var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + string? prevOutboundTag = null; + if (prevNode is not null + && Global.SingboxSupportConfigType.Contains(prevNode.ConfigType)) + { + prevOutboundTag = $"prev-{Global.ProxyTag}"; + var prevServer = await GenServer(prevNode); + prevServer.tag = prevOutboundTag; + if (prevServer is Endpoints4Sbox endpoint) + { + singboxConfig.endpoints ??= new(); + singboxConfig.endpoints.Add(endpoint); + } + else if (prevServer is Outbound4Sbox outboundPrev) + { + singboxConfig.outbounds.Add(outboundPrev); + } + } + var nextServer = await GenChainOutbounds(subItem, outbound, prevOutboundTag); + + if (nextServer is not null) + { + if (nextServer is Endpoints4Sbox endpoint) + { + singboxConfig.endpoints ??= new(); + singboxConfig.endpoints.Insert(0, endpoint); + } + else if (nextServer is Outbound4Sbox outboundNext) + { + singboxConfig.outbounds.Insert(0, outboundNext); + } + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + + return 0; + } + + private async Task GenOutboundsList(List nodes, SingboxConfig singboxConfig) + { + try + { + // Get outbound template and initialize lists + var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); + if (txtOutbound.IsNullOrEmpty()) + { + return 0; + } + + var resultOutbounds = new List(); + var resultEndpoints = new List(); // For endpoints + var prevOutbounds = new List(); // Separate list for prev outbounds + var prevEndpoints = new List(); // Separate list for prev endpoints + var proxyTags = new List(); // For selector and urltest outbounds + + // Cache for chain proxies to avoid duplicate generation + var nextProxyCache = new Dictionary(); + var prevProxyTags = new Dictionary(); // Map from profile name to tag + var prevIndex = 0; // Index for prev outbounds + + // Process each node + var index = 0; + foreach (var node in nodes) + { + index++; + + // Handle proxy chain + string? prevTag = null; + var currentServer = await GenServer(node); + var nextServer = nextProxyCache.GetValueOrDefault(node.Subid, null); + if (nextServer != null) + { + nextServer = JsonUtils.DeepCopy(nextServer); + } + + var subItem = await AppManager.Instance.GetSubItem(node.Subid); + + // current proxy + currentServer.tag = $"{Global.ProxyTag}-{index}"; + proxyTags.Add(currentServer.tag); + + if (!node.Subid.IsNullOrEmpty()) + { + if (prevProxyTags.TryGetValue(node.Subid, out var value)) + { + prevTag = value; // maybe null + } + else + { + var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + if (prevNode is not null + && Global.SingboxSupportConfigType.Contains(prevNode.ConfigType)) + { + var prevOutbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(prevNode, prevOutbound); + prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}"; + prevOutbound.tag = prevTag; + prevOutbounds.Add(prevOutbound); + } + prevProxyTags[node.Subid] = prevTag; + } + + nextServer = await GenChainOutbounds(subItem, currentServer, prevTag, nextServer); + if (!nextProxyCache.ContainsKey(node.Subid)) + { + nextProxyCache[node.Subid] = nextServer; + } + } + + if (nextServer is not null) + { + if (nextServer is Endpoints4Sbox nextEndpoint) + { + resultEndpoints.Add(nextEndpoint); + } + else if (nextServer is Outbound4Sbox nextOutbound) + { + resultOutbounds.Add(nextOutbound); + } + } + if (currentServer is Endpoints4Sbox currentEndpoint) + { + resultEndpoints.Add(currentEndpoint); + } + else if (currentServer is Outbound4Sbox currentOutbound) + { + resultOutbounds.Add(currentOutbound); + } + } + + // Add urltest outbound (auto selection based on latency) + if (proxyTags.Count > 0) + { + var outUrltest = new Outbound4Sbox + { + type = "urltest", + tag = $"{Global.ProxyTag}-auto", + outbounds = proxyTags, + interrupt_exist_connections = false, + }; + + // Add selector outbound (manual selection) + var outSelector = new Outbound4Sbox + { + type = "selector", + tag = Global.ProxyTag, + outbounds = JsonUtils.DeepCopy(proxyTags), + interrupt_exist_connections = false, + }; + outSelector.outbounds.Insert(0, outUrltest.tag); + + // Insert these at the beginning + resultOutbounds.Insert(0, outUrltest); + resultOutbounds.Insert(0, outSelector); + } + + // Merge results: first the selector/urltest/proxies, then other outbounds, and finally prev outbounds + resultOutbounds.AddRange(prevOutbounds); + resultOutbounds.AddRange(singboxConfig.outbounds); + singboxConfig.outbounds = resultOutbounds; + singboxConfig.endpoints ??= new List(); + resultEndpoints.AddRange(singboxConfig.endpoints); + singboxConfig.endpoints = resultEndpoints; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + + return 0; + } + + private async Task GenChainOutbounds(SubItem subItem, BaseServer4Sbox outbound, string? prevOutboundTag, BaseServer4Sbox? nextOutbound = null) + { + try + { + var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); + + if (!prevOutboundTag.IsNullOrEmpty()) + { + outbound.detour = prevOutboundTag; + } + + // Next proxy + var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); + if (nextNode is not null + && Global.SingboxSupportConfigType.Contains(nextNode.ConfigType)) + { + nextOutbound ??= await GenServer(nextNode); + nextOutbound.tag = outbound.tag; + + outbound.tag = $"mid-{outbound.tag}"; + nextOutbound.detour = outbound.tag; + } + return nextOutbound; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return null; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs new file mode 100644 index 00000000..24804c50 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -0,0 +1,365 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task GenRouting(SingboxConfig singboxConfig) + { + try + { + singboxConfig.route.final = Global.ProxyTag; + var item = _config.SimpleDNSItem; + + var defaultDomainResolverTag = Global.SingboxOutboundResolverTag; + var directDNSStrategy = item.SingboxStrategy4Direct.IsNullOrEmpty() ? Global.SingboxDomainStrategy4Out.FirstOrDefault() : item.SingboxStrategy4Direct; + + var rawDNSItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); + if (rawDNSItem != null && rawDNSItem.Enabled == true) + { + defaultDomainResolverTag = Global.SingboxFinalResolverTag; + directDNSStrategy = rawDNSItem.DomainStrategy4Freedom.IsNullOrEmpty() ? Global.SingboxDomainStrategy4Out.FirstOrDefault() : rawDNSItem.DomainStrategy4Freedom; + } + singboxConfig.route.default_domain_resolver = new() + { + server = defaultDomainResolverTag, + strategy = directDNSStrategy + }; + + if (_config.TunModeItem.EnableTun) + { + singboxConfig.route.auto_detect_interface = true; + + var tunRules = JsonUtils.Deserialize>(EmbedUtils.GetEmbedText(Global.TunSingboxRulesFileName)); + if (tunRules != null) + { + singboxConfig.route.rules.AddRange(tunRules); + } + + GenRoutingDirectExe(out var lstDnsExe, out var lstDirectExe); + singboxConfig.route.rules.Add(new() + { + port = new() { 53 }, + action = "hijack-dns", + process_name = lstDnsExe + }); + + singboxConfig.route.rules.Add(new() + { + outbound = Global.DirectTag, + process_name = lstDirectExe + }); + } + + if (_config.Inbound.First().SniffingEnabled) + { + singboxConfig.route.rules.Add(new() + { + action = "sniff" + }); + singboxConfig.route.rules.Add(new() + { + protocol = new() { "dns" }, + action = "hijack-dns" + }); + } + else + { + singboxConfig.route.rules.Add(new() + { + port = new() { 53 }, + network = new() { "udp" }, + action = "hijack-dns" + }); + } + + singboxConfig.route.rules.Add(new() + { + outbound = Global.DirectTag, + clash_mode = ERuleMode.Direct.ToString() + }); + singboxConfig.route.rules.Add(new() + { + outbound = Global.ProxyTag, + clash_mode = ERuleMode.Global.ToString() + }); + + var domainStrategy = _config.RoutingBasicItem.DomainStrategy4Singbox.IsNullOrEmpty() ? null : _config.RoutingBasicItem.DomainStrategy4Singbox; + var defaultRouting = await ConfigHandler.GetDefaultRouting(_config); + if (defaultRouting.DomainStrategy4Singbox.IsNotEmpty()) + { + domainStrategy = defaultRouting.DomainStrategy4Singbox; + } + var resolveRule = new Rule4Sbox + { + action = "resolve", + strategy = domainStrategy + }; + if (_config.RoutingBasicItem.DomainStrategy == Global.IPOnDemand) + { + singboxConfig.route.rules.Add(resolveRule); + } + + var routing = await ConfigHandler.GetDefaultRouting(_config); + var ipRules = new List(); + if (routing != null) + { + var rules = JsonUtils.Deserialize>(routing.RuleSet); + foreach (var item1 in rules ?? []) + { + if (item1.Enabled) + { + await GenRoutingUserRule(item1, singboxConfig); + if (item1.Ip != null && item1.Ip.Count > 0) + { + ipRules.Add(item1); + } + } + } + } + if (_config.RoutingBasicItem.DomainStrategy == Global.IPIfNonMatch) + { + singboxConfig.route.rules.Add(resolveRule); + foreach (var item2 in ipRules) + { + await GenRoutingUserRule(item2, singboxConfig); + } + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private void GenRoutingDirectExe(out List lstDnsExe, out List lstDirectExe) + { + var dnsExeSet = new HashSet(StringComparer.OrdinalIgnoreCase); + var directExeSet = new HashSet(StringComparer.OrdinalIgnoreCase); + + var coreInfoResult = CoreInfoManager.Instance.GetCoreInfo(); + + foreach (var coreConfig in coreInfoResult) + { + if (coreConfig.CoreType == ECoreType.v2rayN) + { + continue; + } + + foreach (var baseExeName in coreConfig.CoreExes) + { + if (coreConfig.CoreType != ECoreType.sing_box) + { + dnsExeSet.Add(Utils.GetExeName(baseExeName)); + } + directExeSet.Add(Utils.GetExeName(baseExeName)); + } + } + + lstDnsExe = new List(dnsExeSet); + lstDirectExe = new List(directExeSet); + } + + private async Task GenRoutingUserRule(RulesItem item, SingboxConfig singboxConfig) + { + try + { + if (item == null) + { + return 0; + } + item.OutboundTag = await GenRoutingUserRuleOutbound(item.OutboundTag, singboxConfig); + var rules = singboxConfig.route.rules; + + var rule = new Rule4Sbox(); + if (item.OutboundTag == "block") + { + rule.action = "reject"; + } + else + { + rule.outbound = item.OutboundTag; + } + + if (item.Port.IsNotEmpty()) + { + var portRanges = item.Port.Split(',').Where(it => it.Contains('-')).Select(it => it.Replace("-", ":")).ToList(); + var ports = item.Port.Split(',').Where(it => !it.Contains('-')).Select(it => it.ToInt()).ToList(); + + rule.port_range = portRanges.Count > 0 ? portRanges : null; + rule.port = ports.Count > 0 ? ports : null; + } + if (item.Network.IsNotEmpty()) + { + rule.network = Utils.String2List(item.Network); + } + if (item.Protocol?.Count > 0) + { + rule.protocol = item.Protocol; + } + if (item.InboundTag?.Count >= 0) + { + rule.inbound = item.InboundTag; + } + var rule1 = JsonUtils.DeepCopy(rule); + var rule2 = JsonUtils.DeepCopy(rule); + var rule3 = JsonUtils.DeepCopy(rule); + + var hasDomainIp = false; + if (item.Domain?.Count > 0) + { + var countDomain = 0; + foreach (var it in item.Domain) + { + if (ParseV2Domain(it, rule1)) + countDomain++; + } + if (countDomain > 0) + { + rules.Add(rule1); + hasDomainIp = true; + } + } + + if (item.Ip?.Count > 0) + { + var countIp = 0; + foreach (var it in item.Ip) + { + if (ParseV2Address(it, rule2)) + countIp++; + } + if (countIp > 0) + { + rules.Add(rule2); + hasDomainIp = true; + } + } + + if (_config.TunModeItem.EnableTun && item.Process?.Count > 0) + { + rule3.process_name = item.Process; + rules.Add(rule3); + hasDomainIp = true; + } + + if (!hasDomainIp + && (rule.port != null || rule.port_range != null || rule.protocol != null || rule.inbound != null || rule.network != null)) + { + rules.Add(rule); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + + private bool ParseV2Domain(string domain, Rule4Sbox rule) + { + if (domain.StartsWith("#") || domain.StartsWith("ext:") || domain.StartsWith("ext-domain:")) + { + return false; + } + else if (domain.StartsWith("geosite:")) + { + rule.geosite ??= []; + rule.geosite?.Add(domain.Substring(8)); + } + else if (domain.StartsWith("regexp:")) + { + rule.domain_regex ??= []; + rule.domain_regex?.Add(domain.Replace(Global.RoutingRuleComma, ",").Substring(7)); + } + else if (domain.StartsWith("domain:")) + { + rule.domain ??= []; + rule.domain_suffix ??= []; + rule.domain?.Add(domain.Substring(7)); + rule.domain_suffix?.Add("." + domain.Substring(7)); + } + else if (domain.StartsWith("full:")) + { + rule.domain ??= []; + rule.domain?.Add(domain.Substring(5)); + } + else if (domain.StartsWith("keyword:")) + { + rule.domain_keyword ??= []; + rule.domain_keyword?.Add(domain.Substring(8)); + } + else + { + rule.domain_keyword ??= []; + rule.domain_keyword?.Add(domain); + } + return true; + } + + private bool ParseV2Address(string address, Rule4Sbox rule) + { + if (address.StartsWith("ext:") || address.StartsWith("ext-ip:")) + { + return false; + } + else if (address.Equals("geoip:private")) + { + rule.ip_is_private = true; + } + else if (address.StartsWith("geoip:")) + { + rule.geoip ??= new(); + rule.geoip?.Add(address.Substring(6)); + } + else if (address.Equals("geoip:!private")) + { + rule.ip_is_private = false; + } + else if (address.StartsWith("geoip:!")) + { + rule.geoip ??= new(); + rule.geoip?.Add(address.Substring(6)); + rule.invert = true; + } + else + { + rule.ip_cidr ??= new(); + rule.ip_cidr?.Add(address); + } + return true; + } + + private async Task GenRoutingUserRuleOutbound(string outboundTag, SingboxConfig singboxConfig) + { + if (Global.OutboundTags.Contains(outboundTag)) + { + return outboundTag; + } + + var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); + if (node == null + || !Global.SingboxSupportConfigType.Contains(node.ConfigType)) + { + return Global.ProxyTag; + } + + var server = await GenServer(node); + if (server is null) + { + return Global.ProxyTag; + } + + server.tag = Global.ProxyTag + node.IndexId.ToString(); + if (server is Endpoints4Sbox endpoint) + { + singboxConfig.endpoints ??= new(); + singboxConfig.endpoints.Add(endpoint); + } + else if (server is Outbound4Sbox outbound) + { + singboxConfig.outbounds.Add(outbound); + } + + return server.tag; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs new file mode 100644 index 00000000..ef611c91 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs @@ -0,0 +1,119 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task ConvertGeo2Ruleset(SingboxConfig singboxConfig) + { + static void AddRuleSets(List ruleSets, List? rule_set) + { + if (rule_set != null) + ruleSets.AddRange(rule_set); + } + var geosite = "geosite"; + var geoip = "geoip"; + var ruleSets = new List(); + + //convert route geosite & geoip to ruleset + foreach (var rule in singboxConfig.route.rules.Where(t => t.geosite?.Count > 0).ToList() ?? []) + { + rule.rule_set ??= new List(); + rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList()); + rule.geosite = null; + AddRuleSets(ruleSets, rule.rule_set); + } + foreach (var rule in singboxConfig.route.rules.Where(t => t.geoip?.Count > 0).ToList() ?? []) + { + rule.rule_set ??= new List(); + rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList()); + rule.geoip = null; + AddRuleSets(ruleSets, rule.rule_set); + } + + //convert dns geosite & geoip to ruleset + foreach (var rule in singboxConfig.dns?.rules.Where(t => t.geosite?.Count > 0).ToList() ?? []) + { + rule.rule_set ??= new List(); + rule.rule_set.AddRange(rule?.geosite?.Select(t => $"{geosite}-{t}").ToList()); + rule.geosite = null; + } + foreach (var rule in singboxConfig.dns?.rules.Where(t => t.geoip?.Count > 0).ToList() ?? []) + { + rule.rule_set ??= new List(); + rule.rule_set.AddRange(rule?.geoip?.Select(t => $"{geoip}-{t}").ToList()); + rule.geoip = null; + } + foreach (var dnsRule in singboxConfig.dns?.rules.Where(t => t.rule_set?.Count > 0).ToList() ?? []) + { + AddRuleSets(ruleSets, dnsRule.rule_set); + } + //rules in rules + foreach (var item in singboxConfig.dns?.rules.Where(t => t.rules?.Count > 0).Select(t => t.rules).ToList() ?? []) + { + foreach (var item2 in item ?? []) + { + AddRuleSets(ruleSets, item2.rule_set); + } + } + + //load custom ruleset file + List customRulesets = []; + + var routing = await ConfigHandler.GetDefaultRouting(_config); + if (routing.CustomRulesetPath4Singbox.IsNotEmpty()) + { + var result = EmbedUtils.LoadResource(routing.CustomRulesetPath4Singbox); + if (result.IsNotEmpty()) + { + customRulesets = (JsonUtils.Deserialize>(result) ?? []) + .Where(t => t.tag != null) + .Where(t => t.type != null) + .Where(t => t.format != null) + .ToList(); + } + } + + //Local srs files address + var localSrss = Utils.GetBinPath("srss"); + + //Add ruleset srs + singboxConfig.route.rule_set = []; + foreach (var item in new HashSet(ruleSets)) + { + if (item.IsNullOrEmpty()) + { continue; } + var customRuleset = customRulesets.FirstOrDefault(t => t.tag != null && t.tag.Equals(item)); + if (customRuleset is null) + { + var pathSrs = Path.Combine(localSrss, $"{item}.srs"); + if (File.Exists(pathSrs)) + { + customRuleset = new() + { + type = "local", + format = "binary", + tag = item, + path = pathSrs + }; + } + else + { + var srsUrl = string.IsNullOrEmpty(_config.ConstItem.SrsSourceUrl) + ? Global.SingboxRulesetUrl + : _config.ConstItem.SrsSourceUrl; + + customRuleset = new() + { + type = "remote", + format = "binary", + tag = item, + url = string.Format(srsUrl, item.StartsWith(geosite) ? geosite : geoip, item), + download_detour = Global.ProxyTag + }; + } + } + singboxConfig.route.rule_set.Add(customRuleset); + } + + return 0; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxStatisticService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxStatisticService.cs new file mode 100644 index 00000000..c3acd810 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxStatisticService.cs @@ -0,0 +1,29 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigSingboxService +{ + private async Task GenExperimental(SingboxConfig singboxConfig) + { + //if (_config.guiItem.enableStatistics) + { + singboxConfig.experimental ??= new Experimental4Sbox(); + singboxConfig.experimental.clash_api = new Clash_Api4Sbox() + { + external_controller = $"{Global.Loopback}:{AppManager.Instance.StatePort2}", + }; + } + + if (_config.CoreBasicItem.EnableCacheFile4Sbox) + { + singboxConfig.experimental ??= new Experimental4Sbox(); + singboxConfig.experimental.cache_file = new CacheFile4Sbox() + { + enabled = true, + path = Utils.GetBinPath("cache.db"), + store_fakeip = _config.SimpleDNSItem.FakeIP == true + }; + } + + return await Task.FromResult(0); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs new file mode 100644 index 00000000..b6148580 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs @@ -0,0 +1,411 @@ +using System.Net; +using System.Net.NetworkInformation; + +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService(Config config) +{ + private readonly Config _config = config; + private static readonly string _tag = "CoreConfigV2rayService"; + + #region public gen function + + public async Task GenerateClientConfigContent(ProfileItem node) + { + var ret = new RetResult(); + try + { + if (node == null + || node.Port <= 0) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + + if (node.GetNetwork() is nameof(ETransport.quic)) + { + ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}"; + return ret; + } + + ret.Msg = ResUI.InitialConfiguration; + + var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); + if (result.IsNullOrEmpty()) + { + ret.Msg = ResUI.FailedGetDefaultConfiguration; + return ret; + } + + var v2rayConfig = JsonUtils.Deserialize(result); + if (v2rayConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + await GenLog(v2rayConfig); + + await GenInbounds(v2rayConfig); + + await GenOutbound(node, v2rayConfig.outbounds.First()); + + await GenMoreOutbounds(node, v2rayConfig); + + await GenRouting(v2rayConfig); + + await GenDns(node, v2rayConfig); + + await GenStatistic(v2rayConfig); + + ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); + ret.Success = true; + ret.Data = await ApplyFullConfigTemplate(v2rayConfig); + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + + public async Task GenerateClientMultipleLoadConfig(List selecteds, EMultipleLoad multipleLoad) + { + var ret = new RetResult(); + + try + { + if (_config == null) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + + ret.Msg = ResUI.InitialConfiguration; + + string result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); + string txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty()) + { + ret.Msg = ResUI.FailedGetDefaultConfiguration; + return ret; + } + + var v2rayConfig = JsonUtils.Deserialize(result); + if (v2rayConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + await GenLog(v2rayConfig); + await GenInbounds(v2rayConfig); + await GenRouting(v2rayConfig); + await GenDns(null, v2rayConfig); + await GenStatistic(v2rayConfig); + v2rayConfig.outbounds.RemoveAt(0); + + var proxyProfiles = new List(); + foreach (var it in selecteds) + { + if (!Global.XraySupportConfigType.Contains(it.ConfigType)) + { + continue; + } + if (it.Port <= 0) + { + continue; + } + var item = await AppManager.Instance.GetProfileItem(it.IndexId); + if (item is null) + { + continue; + } + if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) + { + if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) + { + continue; + } + } + if (item.ConfigType == EConfigType.Shadowsocks + && !Global.SsSecuritiesInSingbox.Contains(item.Security)) + { + continue; + } + if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow)) + { + continue; + } + + //outbound + proxyProfiles.Add(item); + } + if (proxyProfiles.Count <= 0) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + await GenOutboundsList(proxyProfiles, v2rayConfig); + + //add balancers + await GenBalancer(v2rayConfig, multipleLoad); + + var balancer = v2rayConfig.routing.balancers.First(); + + //add rule + var rules = v2rayConfig.routing.rules.Where(t => t.outboundTag == Global.ProxyTag).ToList(); + if (rules?.Count > 0) + { + foreach (var rule in rules) + { + rule.outboundTag = null; + rule.balancerTag = balancer.tag; + } + } + if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch) + { + v2rayConfig.routing.rules.Add(new() + { + ip = ["0.0.0.0/0", "::/0"], + balancerTag = balancer.tag, + type = "field" + }); + } + else + { + v2rayConfig.routing.rules.Add(new() + { + network = "tcp,udp", + balancerTag = balancer.tag, + type = "field" + }); + } + + ret.Success = true; + + ret.Data = await ApplyFullConfigTemplate(v2rayConfig, true); + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + + public async Task GenerateClientSpeedtestConfig(List selecteds) + { + var ret = new RetResult(); + try + { + if (_config == null) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + + ret.Msg = ResUI.InitialConfiguration; + + var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty()) + { + ret.Msg = ResUI.FailedGetDefaultConfiguration; + return ret; + } + + var v2rayConfig = JsonUtils.Deserialize(result); + if (v2rayConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + List lstIpEndPoints = new(); + List lstTcpConns = new(); + try + { + lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners()); + lstIpEndPoints.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners()); + lstTcpConns.AddRange(IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections()); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + + await GenLog(v2rayConfig); + v2rayConfig.inbounds.Clear(); + v2rayConfig.outbounds.Clear(); + v2rayConfig.routing.rules.Clear(); + + var initPort = AppManager.Instance.GetLocalPort(EInboundProtocol.speedtest); + + foreach (var it in selecteds) + { + if (!Global.XraySupportConfigType.Contains(it.ConfigType)) + { + continue; + } + if (it.Port <= 0) + { + continue; + } + var item = await AppManager.Instance.GetProfileItem(it.IndexId); + if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) + { + if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) + { + continue; + } + } + + //find unused port + var port = initPort; + for (var k = initPort; k < Global.MaxPort; k++) + { + if (lstIpEndPoints?.FindIndex(_it => _it.Port == k) >= 0) + { + continue; + } + if (lstTcpConns?.FindIndex(_it => _it.LocalEndPoint.Port == k) >= 0) + { + continue; + } + //found + port = k; + initPort = port + 1; + break; + } + + //Port In Used + if (lstIpEndPoints?.FindIndex(_it => _it.Port == port) >= 0) + { + continue; + } + it.Port = port; + it.AllowTest = true; + + //outbound + if (item is null) + { + continue; + } + if (item.ConfigType == EConfigType.Shadowsocks + && !Global.SsSecuritiesInXray.Contains(item.Security)) + { + continue; + } + if (item.ConfigType == EConfigType.VLESS + && !Global.Flows.Contains(item.Flow)) + { + continue; + } + if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan + && item.StreamSecurity == Global.StreamSecurityReality + && item.PublicKey.IsNullOrEmpty()) + { + continue; + } + + //inbound + Inbounds4Ray inbound = new() + { + listen = Global.Loopback, + port = port, + protocol = EInboundProtocol.mixed.ToString(), + }; + inbound.tag = inbound.protocol + inbound.port.ToString(); + v2rayConfig.inbounds.Add(inbound); + + var outbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(item, outbound); + outbound.tag = Global.ProxyTag + inbound.port.ToString(); + v2rayConfig.outbounds.Add(outbound); + + //rule + RulesItem4Ray rule = new() + { + inboundTag = new List { inbound.tag }, + outboundTag = outbound.tag, + type = "field" + }; + v2rayConfig.routing.rules.Add(rule); + } + + //ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary()); + ret.Success = true; + ret.Data = JsonUtils.Serialize(v2rayConfig); + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + + public async Task GenerateClientSpeedtestConfig(ProfileItem node, int port) + { + var ret = new RetResult(); + try + { + if (node is not { Port: > 0 }) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + + if (node.GetNetwork() is nameof(ETransport.quic)) + { + ret.Msg = ResUI.Incorrectconfiguration + $" - {node.GetNetwork()}"; + return ret; + } + + var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); + if (result.IsNullOrEmpty()) + { + ret.Msg = ResUI.FailedGetDefaultConfiguration; + return ret; + } + + var v2rayConfig = JsonUtils.Deserialize(result); + if (v2rayConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + await GenLog(v2rayConfig); + await GenOutbound(node, v2rayConfig.outbounds.First()); + await GenMoreOutbounds(node, v2rayConfig); + + v2rayConfig.routing.rules.Clear(); + v2rayConfig.inbounds.Clear(); + v2rayConfig.inbounds.Add(new() + { + tag = $"{EInboundProtocol.socks}{port}", + listen = Global.Loopback, + port = port, + protocol = EInboundProtocol.mixed.ToString(), + }); + + ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); + ret.Success = true; + ret.Data = JsonUtils.Serialize(v2rayConfig); + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + + #endregion public gen function +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs new file mode 100644 index 00000000..8d2476e6 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs @@ -0,0 +1,50 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad) + { + if (multipleLoad == EMultipleLoad.LeastPing) + { + var observatory = new Observatory4Ray + { + subjectSelector = [Global.ProxyTag], + probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, + probeInterval = "3m", + enableConcurrency = true, + }; + v2rayConfig.observatory = observatory; + } + else if (multipleLoad == EMultipleLoad.LeastLoad) + { + var burstObservatory = new BurstObservatory4Ray + { + subjectSelector = [Global.ProxyTag], + pingConfig = new() + { + destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, + interval = "5m", + timeout = "30s", + sampling = 2, + } + }; + v2rayConfig.burstObservatory = burstObservatory; + } + var strategyType = multipleLoad switch + { + EMultipleLoad.Random => "random", + EMultipleLoad.RoundRobin => "roundRobin", + EMultipleLoad.LeastPing => "leastPing", + EMultipleLoad.LeastLoad => "leastLoad", + _ => "roundRobin", + }; + var balancer = new BalancersItem4Ray + { + selector = [Global.ProxyTag], + strategy = new() { type = strategyType }, + tag = $"{Global.ProxyTag}-round", + }; + v2rayConfig.routing.balancers = [balancer]; + return await Task.FromResult(0); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs new file mode 100644 index 00000000..5d1f7d63 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs @@ -0,0 +1,86 @@ +using System.Text.Json.Nodes; + +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task ApplyFullConfigTemplate(V2rayConfig v2rayConfig, bool handleBalancerAndRules = false) + { + var fullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray); + if (fullConfigTemplate == null || !fullConfigTemplate.Enabled || fullConfigTemplate.Config.IsNullOrEmpty()) + { + return JsonUtils.Serialize(v2rayConfig); + } + + var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplate.Config); + if (fullConfigTemplateNode == null) + { + return JsonUtils.Serialize(v2rayConfig); + } + + // Handle balancer and rules modifications (for multiple load scenarios) + if (handleBalancerAndRules && v2rayConfig.routing?.balancers?.Count > 0) + { + var balancer = v2rayConfig.routing.balancers.First(); + + // Modify existing rules in custom config + var rulesNode = fullConfigTemplateNode["routing"]?["rules"]; + if (rulesNode != null) + { + foreach (var rule in rulesNode.AsArray()) + { + if (rule["outboundTag"]?.GetValue() == Global.ProxyTag) + { + rule.AsObject().Remove("outboundTag"); + rule["balancerTag"] = balancer.tag; + } + } + } + + // Ensure routing node exists + if (fullConfigTemplateNode["routing"] == null) + { + fullConfigTemplateNode["routing"] = new JsonObject(); + } + + // Handle balancers - append instead of override + if (fullConfigTemplateNode["routing"]["balancers"] is JsonArray customBalancersNode) + { + if (JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.routing.balancers)) is JsonArray newBalancers) + { + foreach (var balancerNode in newBalancers) + { + customBalancersNode.Add(balancerNode?.DeepClone()); + } + } + } + else + { + fullConfigTemplateNode["routing"]["balancers"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.routing.balancers)); + } + } + + // Handle outbounds - append instead of override + var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray(); + foreach (var outbound in v2rayConfig.outbounds) + { + if (outbound.protocol.ToLower() is "blackhole" or "dns" or "freedom") + { + if (fullConfigTemplate.AddProxyOnly == true) + { + continue; + } + } + else if ((outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() == true) && (!fullConfigTemplate.ProxyDetour.IsNullOrEmpty()) && !(Utils.IsPrivateNetwork(outbound.settings?.servers?.FirstOrDefault()?.address ?? string.Empty) || Utils.IsPrivateNetwork(outbound.settings?.vnext?.FirstOrDefault()?.address ?? string.Empty))) + { + outbound.streamSettings ??= new StreamSettings4Ray(); + outbound.streamSettings.sockopt ??= new Sockopt4Ray(); + outbound.streamSettings.sockopt.dialerProxy = fullConfigTemplate.ProxyDetour; + } + customOutboundsNode.Add(JsonUtils.DeepCopy(outbound)); + } + fullConfigTemplateNode["outbounds"] = customOutboundsNode; + + return await Task.FromResult(JsonUtils.Serialize(fullConfigTemplateNode)); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs new file mode 100644 index 00000000..6f64e923 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs @@ -0,0 +1,410 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task GenDns(ProfileItem? node, V2rayConfig v2rayConfig) + { + try + { + var item = await AppManager.Instance.GetDNSItem(ECoreType.Xray); + if (item != null && item.Enabled == true) + { + var result = await GenDnsCompatible(node, v2rayConfig); + + if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch) + { + // DNS routing + v2rayConfig.dns.tag = Global.DnsTag; + v2rayConfig.routing.rules.Add(new RulesItem4Ray + { + type = "field", + inboundTag = new List { Global.DnsTag }, + outboundTag = Global.ProxyTag, + }); + } + + return result; + } + var simpleDNSItem = _config.SimpleDNSItem; + var domainStrategy4Freedom = simpleDNSItem?.RayStrategy4Freedom; + + //Outbound Freedom domainStrategy + if (domainStrategy4Freedom.IsNotEmpty()) + { + var outbound = v2rayConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag }); + if (outbound != null) + { + outbound.settings = new() + { + domainStrategy = domainStrategy4Freedom, + userLevel = 0 + }; + } + } + + await GenDnsServers(node, v2rayConfig, simpleDNSItem); + await GenDnsHosts(v2rayConfig, simpleDNSItem); + + if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch) + { + // DNS routing + v2rayConfig.dns.tag = Global.DnsTag; + v2rayConfig.routing.rules.Add(new RulesItem4Ray + { + type = "field", + inboundTag = new List { Global.DnsTag }, + outboundTag = Global.ProxyTag, + }); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenDnsServers(ProfileItem? node, V2rayConfig v2rayConfig, SimpleDNSItem simpleDNSItem) + { + static List ParseDnsAddresses(string? dnsInput, string defaultAddress) + { + var addresses = dnsInput?.Split(dnsInput.Contains(',') ? ',' : ';') + .Select(addr => addr.Trim()) + .Where(addr => !string.IsNullOrEmpty(addr)) + .Select(addr => addr.StartsWith("dhcp", StringComparison.OrdinalIgnoreCase) ? "localhost" : addr) + .Distinct() + .ToList() ?? new List { defaultAddress }; + return addresses.Count > 0 ? addresses : new List { defaultAddress }; + } + + static object CreateDnsServer(string dnsAddress, List domains, List? expectedIPs = null) + { + var dnsServer = new DnsServer4Ray + { + address = dnsAddress, + skipFallback = true, + domains = domains.Count > 0 ? domains : null, + expectedIPs = expectedIPs?.Count > 0 ? expectedIPs : null + }; + return JsonUtils.SerializeToNode(dnsServer, new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + } + + var directDNSAddress = ParseDnsAddresses(simpleDNSItem?.DirectDNS, Global.DomainDirectDNSAddress.FirstOrDefault()); + var remoteDNSAddress = ParseDnsAddresses(simpleDNSItem?.RemoteDNS, Global.DomainRemoteDNSAddress.FirstOrDefault()); + + var directDomainList = new List(); + var directGeositeList = new List(); + var proxyDomainList = new List(); + var proxyGeositeList = new List(); + var expectedDomainList = new List(); + var expectedIPs = new List(); + var regionNames = new HashSet(); + + if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs)) + { + expectedIPs = simpleDNSItem.DirectExpectedIPs + .Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToList(); + + foreach (var ip in expectedIPs) + { + if (ip.StartsWith("geoip:", StringComparison.OrdinalIgnoreCase)) + { + var region = ip["geoip:".Length..]; + if (!string.IsNullOrEmpty(region)) + { + regionNames.Add($"geosite:{region}"); + regionNames.Add($"geosite:geolocation-{region}"); + regionNames.Add($"geosite:tld-{region}"); + } + } + } + } + + var routing = await ConfigHandler.GetDefaultRouting(_config); + List? rules = null; + if (routing != null) + { + rules = JsonUtils.Deserialize>(routing.RuleSet) ?? []; + foreach (var item in rules) + { + if (!item.Enabled || item.Domain is null || item.Domain.Count == 0) + { + continue; + } + + foreach (var domain in item.Domain) + { + if (domain.StartsWith('#')) + continue; + var normalizedDomain = domain.Replace(Global.RoutingRuleComma, ","); + + if (item.OutboundTag == Global.DirectTag) + { + if (normalizedDomain.StartsWith("geosite:")) + { + (regionNames.Contains(normalizedDomain) ? expectedDomainList : directGeositeList).Add(normalizedDomain); + } + else + { + directDomainList.Add(normalizedDomain); + } + } + else if (item.OutboundTag != Global.BlockTag) + { + if (normalizedDomain.StartsWith("geosite:")) + { + proxyGeositeList.Add(normalizedDomain); + } + else + { + proxyDomainList.Add(normalizedDomain); + } + } + } + } + } + + if (Utils.IsDomain(node?.Address)) + { + directDomainList.Add(node.Address); + } + + if (node?.Subid is not null) + { + var subItem = await AppManager.Instance.GetSubItem(node.Subid); + if (subItem is not null) + { + foreach (var profile in new[] { subItem.PrevProfile, subItem.NextProfile }) + { + var profileNode = await AppManager.Instance.GetProfileItemViaRemarks(profile); + if (profileNode is not null + && Global.XraySupportConfigType.Contains(profileNode.ConfigType) + && Utils.IsDomain(profileNode.Address)) + { + directDomainList.Add(profileNode.Address); + } + } + } + } + + v2rayConfig.dns ??= new Dns4Ray(); + v2rayConfig.dns.servers ??= new List(); + + void AddDnsServers(List dnsAddresses, List domains, List? expectedIPs = null) + { + if (domains.Count > 0) + { + foreach (var dnsAddress in dnsAddresses) + { + v2rayConfig.dns.servers.Add(CreateDnsServer(dnsAddress, domains, expectedIPs)); + } + } + } + + AddDnsServers(remoteDNSAddress, proxyDomainList); + AddDnsServers(directDNSAddress, directDomainList); + AddDnsServers(remoteDNSAddress, proxyGeositeList); + AddDnsServers(directDNSAddress, directGeositeList); + AddDnsServers(directDNSAddress, expectedDomainList, expectedIPs); + + var useDirectDns = rules?.LastOrDefault() is { } lastRule + && lastRule.OutboundTag == Global.DirectTag + && (lastRule.Port == "0-65535" + || lastRule.Network == "tcp,udp" + || lastRule.Ip?.Contains("0.0.0.0/0") == true); + + var defaultDnsServers = useDirectDns ? directDNSAddress : remoteDNSAddress; + v2rayConfig.dns.servers.AddRange(defaultDnsServers); + + return 0; + } + + private async Task GenDnsHosts(V2rayConfig v2rayConfig, SimpleDNSItem simpleDNSItem) + { + if (simpleDNSItem.AddCommonHosts == false && simpleDNSItem.UseSystemHosts == false && simpleDNSItem.Hosts.IsNullOrEmpty()) + { + return await Task.FromResult(0); + } + v2rayConfig.dns ??= new Dns4Ray(); + v2rayConfig.dns.hosts ??= new Dictionary(); + if (simpleDNSItem.AddCommonHosts == true) + { + v2rayConfig.dns.hosts = Global.PredefinedHosts.ToDictionary( + kvp => kvp.Key, + kvp => (object)kvp.Value + ); + } + + if (simpleDNSItem.UseSystemHosts == true) + { + var systemHosts = Utils.GetSystemHosts(); + var normalHost = v2rayConfig?.dns?.hosts; + + if (normalHost != null && systemHosts?.Count > 0) + { + foreach (var host in systemHosts) + { + normalHost.TryAdd(host.Key, new List { host.Value }); + } + } + } + + if (!simpleDNSItem.Hosts.IsNullOrEmpty()) + { + var userHostsMap = simpleDNSItem.Hosts + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .Where(line => !string.IsNullOrWhiteSpace(line) && line.Contains(' ')) + .Select(line => line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries)) + .Where(parts => parts.Length >= 2) + .GroupBy(parts => parts[0]) + .ToDictionary( + group => group.Key, + group => group.SelectMany(parts => parts.Skip(1)).ToList() + ); + + foreach (var kvp in userHostsMap) + { + v2rayConfig.dns.hosts[kvp.Key] = kvp.Value; + } + } + return await Task.FromResult(0); + } + + private async Task GenDnsCompatible(ProfileItem? node, V2rayConfig v2rayConfig) + { + try + { + var item = await AppManager.Instance.GetDNSItem(ECoreType.Xray); + var normalDNS = item?.NormalDNS; + var domainStrategy4Freedom = item?.DomainStrategy4Freedom; + if (normalDNS.IsNullOrEmpty()) + { + normalDNS = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName); + } + + //Outbound Freedom domainStrategy + if (domainStrategy4Freedom.IsNotEmpty()) + { + var outbound = v2rayConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag }); + if (outbound != null) + { + outbound.settings = new(); + outbound.settings.domainStrategy = domainStrategy4Freedom; + outbound.settings.userLevel = 0; + } + } + + var obj = JsonUtils.ParseJson(normalDNS); + if (obj is null) + { + List servers = []; + string[] arrDNS = normalDNS.Split(','); + foreach (string str in arrDNS) + { + servers.Add(str); + } + obj = JsonUtils.ParseJson("{}"); + obj["servers"] = JsonUtils.SerializeToNode(servers); + } + + // Append to dns settings + if (item.UseSystemHosts) + { + var systemHosts = Utils.GetSystemHosts(); + if (systemHosts.Count > 0) + { + var normalHost1 = obj["hosts"]; + if (normalHost1 != null) + { + foreach (var host in systemHosts) + { + if (normalHost1[host.Key] != null) + continue; + normalHost1[host.Key] = host.Value; + } + } + } + } + var normalHost = obj["hosts"]; + if (normalHost != null) + { + foreach (var hostProp in normalHost.AsObject().ToList()) + { + if (hostProp.Value is JsonValue value && value.TryGetValue(out var ip)) + { + normalHost[hostProp.Key] = new JsonArray(ip); + } + } + } + + await GenDnsDomainsCompatible(node, obj, item); + + v2rayConfig.dns = JsonUtils.Deserialize(JsonUtils.Serialize(obj)); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenDnsDomainsCompatible(ProfileItem? node, JsonNode dns, DNSItem? dNSItem) + { + if (node == null) + { + return 0; + } + var servers = dns["servers"]; + if (servers != null) + { + var domainList = new List(); + if (Utils.IsDomain(node.Address)) + { + domainList.Add(node.Address); + } + var subItem = await AppManager.Instance.GetSubItem(node.Subid); + if (subItem is not null) + { + // Previous proxy + var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + if (prevNode is not null + && Global.SingboxSupportConfigType.Contains(prevNode.ConfigType) + && Utils.IsDomain(prevNode.Address)) + { + domainList.Add(prevNode.Address); + } + + // Next proxy + var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); + if (nextNode is not null + && Global.SingboxSupportConfigType.Contains(nextNode.ConfigType) + && Utils.IsDomain(nextNode.Address)) + { + domainList.Add(nextNode.Address); + } + } + if (domainList.Count > 0) + { + var dnsServer = new DnsServer4Ray() + { + address = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dNSItem?.DomainDNSAddress, + skipFallback = true, + domains = domainList + }; + servers.AsArray().Add(JsonUtils.SerializeToNode(dnsServer)); + } + } + return await Task.FromResult(0); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs new file mode 100644 index 00000000..7753c21e --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs @@ -0,0 +1,72 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task GenInbounds(V2rayConfig v2rayConfig) + { + try + { + var listen = "0.0.0.0"; + v2rayConfig.inbounds = []; + + var inbound = GetInbound(_config.Inbound.First(), EInboundProtocol.socks, true); + v2rayConfig.inbounds.Add(inbound); + + if (_config.Inbound.First().SecondLocalPortEnabled) + { + var inbound2 = GetInbound(_config.Inbound.First(), EInboundProtocol.socks2, true); + v2rayConfig.inbounds.Add(inbound2); + } + + if (_config.Inbound.First().AllowLANConn) + { + if (_config.Inbound.First().NewPort4LAN) + { + var inbound3 = GetInbound(_config.Inbound.First(), EInboundProtocol.socks3, true); + inbound3.listen = listen; + v2rayConfig.inbounds.Add(inbound3); + + //auth + if (_config.Inbound.First().User.IsNotEmpty() && _config.Inbound.First().Pass.IsNotEmpty()) + { + inbound3.settings.auth = "password"; + inbound3.settings.accounts = new List { new AccountsItem4Ray() { user = _config.Inbound.First().User, pass = _config.Inbound.First().Pass } }; + } + } + else + { + inbound.listen = listen; + } + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + + private Inbounds4Ray GetInbound(InItem inItem, EInboundProtocol protocol, bool bSocks) + { + string result = EmbedUtils.GetEmbedText(Global.V2raySampleInbound); + if (result.IsNullOrEmpty()) + { + return new(); + } + + var inbound = JsonUtils.Deserialize(result); + if (inbound == null) + { + return new(); + } + inbound.tag = protocol.ToString(); + inbound.port = inItem.LocalPort + (int)protocol; + inbound.protocol = EInboundProtocol.mixed.ToString(); + inbound.settings.udp = inItem.UdpEnabled; + inbound.sniffing.enabled = inItem.SniffingEnabled; + inbound.sniffing.destOverride = inItem.DestOverride; + inbound.sniffing.routeOnly = inItem.RouteOnly; + + return inbound; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayLogService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayLogService.cs new file mode 100644 index 00000000..5b9344fb --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayLogService.cs @@ -0,0 +1,29 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task GenLog(V2rayConfig v2rayConfig) + { + try + { + if (_config.CoreBasicItem.LogEnabled) + { + var dtNow = DateTime.Now; + v2rayConfig.log.loglevel = _config.CoreBasicItem.Loglevel; + v2rayConfig.log.access = Utils.GetLogPath($"Vaccess_{dtNow:yyyy-MM-dd}.txt"); + v2rayConfig.log.error = Utils.GetLogPath($"Verror_{dtNow:yyyy-MM-dd}.txt"); + } + else + { + v2rayConfig.log.loglevel = _config.CoreBasicItem.Loglevel; + v2rayConfig.log.access = null; + v2rayConfig.log.error = null; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs new file mode 100644 index 00000000..11e8a8fa --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -0,0 +1,695 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task GenOutbound(ProfileItem node, Outbounds4Ray outbound) + { + try + { + var muxEnabled = node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled; + switch (node.ConfigType) + { + case EConfigType.VMess: + { + VnextItem4Ray vnextItem; + if (outbound.settings.vnext.Count <= 0) + { + vnextItem = new VnextItem4Ray(); + outbound.settings.vnext.Add(vnextItem); + } + else + { + vnextItem = outbound.settings.vnext.First(); + } + vnextItem.address = node.Address; + vnextItem.port = node.Port; + + UsersItem4Ray usersItem; + if (vnextItem.users.Count <= 0) + { + usersItem = new UsersItem4Ray(); + vnextItem.users.Add(usersItem); + } + else + { + usersItem = vnextItem.users.First(); + } + + usersItem.id = node.Id; + usersItem.alterId = node.AlterId; + usersItem.email = Global.UserEMail; + if (Global.VmessSecurities.Contains(node.Security)) + { + usersItem.security = node.Security; + } + else + { + usersItem.security = Global.DefaultSecurity; + } + + await GenOutboundMux(node, outbound, muxEnabled, muxEnabled); + + outbound.settings.servers = null; + break; + } + case EConfigType.Shadowsocks: + { + ServersItem4Ray serversItem; + if (outbound.settings.servers.Count <= 0) + { + serversItem = new ServersItem4Ray(); + outbound.settings.servers.Add(serversItem); + } + else + { + serversItem = outbound.settings.servers.First(); + } + serversItem.address = node.Address; + serversItem.port = node.Port; + serversItem.password = node.Id; + serversItem.method = AppManager.Instance.GetShadowsocksSecurities(node).Contains(node.Security) ? node.Security : "none"; + + serversItem.ota = false; + serversItem.level = 1; + + await GenOutboundMux(node, outbound); + + outbound.settings.vnext = null; + break; + } + case EConfigType.SOCKS: + case EConfigType.HTTP: + { + ServersItem4Ray serversItem; + if (outbound.settings.servers.Count <= 0) + { + serversItem = new ServersItem4Ray(); + outbound.settings.servers.Add(serversItem); + } + else + { + serversItem = outbound.settings.servers.First(); + } + serversItem.address = node.Address; + serversItem.port = node.Port; + serversItem.method = null; + serversItem.password = null; + + if (node.Security.IsNotEmpty() + && node.Id.IsNotEmpty()) + { + SocksUsersItem4Ray socksUsersItem = new() + { + user = node.Security, + pass = node.Id, + level = 1 + }; + + serversItem.users = new List() { socksUsersItem }; + } + + await GenOutboundMux(node, outbound); + + outbound.settings.vnext = null; + break; + } + case EConfigType.VLESS: + { + VnextItem4Ray vnextItem; + if (outbound.settings.vnext?.Count <= 0) + { + vnextItem = new VnextItem4Ray(); + outbound.settings.vnext.Add(vnextItem); + } + else + { + vnextItem = outbound.settings.vnext.First(); + } + vnextItem.address = node.Address; + vnextItem.port = node.Port; + + UsersItem4Ray usersItem; + if (vnextItem.users.Count <= 0) + { + usersItem = new UsersItem4Ray(); + vnextItem.users.Add(usersItem); + } + else + { + usersItem = vnextItem.users.First(); + } + usersItem.id = node.Id; + usersItem.email = Global.UserEMail; + usersItem.encryption = node.Security; + + if (node.Flow.IsNullOrEmpty()) + { + await GenOutboundMux(node, outbound, muxEnabled, muxEnabled); + } + else + { + usersItem.flow = node.Flow; + await GenOutboundMux(node, outbound, false, muxEnabled); + } + outbound.settings.servers = null; + break; + } + case EConfigType.Trojan: + { + ServersItem4Ray serversItem; + if (outbound.settings.servers.Count <= 0) + { + serversItem = new ServersItem4Ray(); + outbound.settings.servers.Add(serversItem); + } + else + { + serversItem = outbound.settings.servers.First(); + } + serversItem.address = node.Address; + serversItem.port = node.Port; + serversItem.password = node.Id; + + serversItem.ota = false; + serversItem.level = 1; + + await GenOutboundMux(node, outbound); + + outbound.settings.vnext = null; + break; + } + case EConfigType.WireGuard: + { + var peer = new WireguardPeer4Ray + { + publicKey = node.PublicKey, + endpoint = node.Address + ":" + node.Port.ToString() + }; + var setting = new Outboundsettings4Ray + { + address = Utils.String2List(node.RequestHost), + secretKey = node.Id, + reserved = Utils.String2List(node.Path)?.Select(int.Parse).ToList(), + mtu = node.ShortId.IsNullOrEmpty() ? Global.TunMtus.First() : node.ShortId.ToInt(), + peers = new List { peer } + }; + outbound.settings = setting; + outbound.settings.vnext = null; + outbound.settings.servers = null; + break; + } + } + + outbound.protocol = Global.ProtocolTypes[node.ConfigType]; + await GenBoundStreamSettings(node, outbound); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenOutboundMux(ProfileItem node, Outbounds4Ray outbound, bool enabledTCP = false, bool enabledUDP = false) + { + try + { + outbound.mux.enabled = false; + outbound.mux.concurrency = -1; + + if (enabledTCP) + { + outbound.mux.enabled = true; + outbound.mux.concurrency = _config.Mux4RayItem.Concurrency; + } + else if (enabledUDP) + { + outbound.mux.enabled = true; + outbound.mux.xudpConcurrency = _config.Mux4RayItem.XudpConcurrency; + outbound.mux.xudpProxyUDP443 = _config.Mux4RayItem.XudpProxyUDP443; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + + private async Task GenBoundStreamSettings(ProfileItem node, Outbounds4Ray outbound) + { + try + { + var streamSettings = outbound.streamSettings; + streamSettings.network = node.GetNetwork(); + var host = node.RequestHost.TrimEx(); + var path = node.Path.TrimEx(); + var sni = node.Sni.TrimEx(); + var useragent = ""; + if (!_config.CoreBasicItem.DefUserAgent.IsNullOrEmpty()) + { + try + { + useragent = Global.UserAgentTexts[_config.CoreBasicItem.DefUserAgent]; + } + catch (KeyNotFoundException) + { + useragent = _config.CoreBasicItem.DefUserAgent; + } + } + + //if tls + if (node.StreamSecurity == Global.StreamSecurity) + { + streamSettings.security = node.StreamSecurity; + + TlsSettings4Ray tlsSettings = new() + { + allowInsecure = Utils.ToBool(node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : node.AllowInsecure), + alpn = node.GetAlpn(), + fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint + }; + if (sni.IsNotEmpty()) + { + tlsSettings.serverName = sni; + } + else if (host.IsNotEmpty()) + { + tlsSettings.serverName = Utils.String2List(host)?.First(); + } + streamSettings.tlsSettings = tlsSettings; + } + + //if Reality + if (node.StreamSecurity == Global.StreamSecurityReality) + { + streamSettings.security = node.StreamSecurity; + + TlsSettings4Ray realitySettings = new() + { + fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint, + serverName = sni, + publicKey = node.PublicKey, + shortId = node.ShortId, + spiderX = node.SpiderX, + mldsa65Verify = node.Mldsa65Verify, + show = false, + }; + + streamSettings.realitySettings = realitySettings; + } + + //streamSettings + switch (node.GetNetwork()) + { + case nameof(ETransport.kcp): + KcpSettings4Ray kcpSettings = new() + { + mtu = _config.KcpItem.Mtu, + tti = _config.KcpItem.Tti + }; + + kcpSettings.uplinkCapacity = _config.KcpItem.UplinkCapacity; + kcpSettings.downlinkCapacity = _config.KcpItem.DownlinkCapacity; + + kcpSettings.congestion = _config.KcpItem.Congestion; + kcpSettings.readBufferSize = _config.KcpItem.ReadBufferSize; + kcpSettings.writeBufferSize = _config.KcpItem.WriteBufferSize; + kcpSettings.header = new Header4Ray + { + type = node.HeaderType, + domain = host.IsNullOrEmpty() ? null : host + }; + if (path.IsNotEmpty()) + { + kcpSettings.seed = path; + } + streamSettings.kcpSettings = kcpSettings; + break; + //ws + case nameof(ETransport.ws): + WsSettings4Ray wsSettings = new(); + wsSettings.headers = new Headers4Ray(); + + if (host.IsNotEmpty()) + { + wsSettings.host = host; + wsSettings.headers.Host = host; + } + if (path.IsNotEmpty()) + { + wsSettings.path = path; + } + if (useragent.IsNotEmpty()) + { + wsSettings.headers.UserAgent = useragent; + } + streamSettings.wsSettings = wsSettings; + + break; + //httpupgrade + case nameof(ETransport.httpupgrade): + HttpupgradeSettings4Ray httpupgradeSettings = new(); + + if (path.IsNotEmpty()) + { + httpupgradeSettings.path = path; + } + if (host.IsNotEmpty()) + { + httpupgradeSettings.host = host; + } + streamSettings.httpupgradeSettings = httpupgradeSettings; + + break; + //xhttp + case nameof(ETransport.xhttp): + streamSettings.network = ETransport.xhttp.ToString(); + XhttpSettings4Ray xhttpSettings = new(); + + if (path.IsNotEmpty()) + { + xhttpSettings.path = path; + } + if (host.IsNotEmpty()) + { + xhttpSettings.host = host; + } + if (node.HeaderType.IsNotEmpty() && Global.XhttpMode.Contains(node.HeaderType)) + { + xhttpSettings.mode = node.HeaderType; + } + if (node.Extra.IsNotEmpty()) + { + xhttpSettings.extra = JsonUtils.ParseJson(node.Extra); + } + + streamSettings.xhttpSettings = xhttpSettings; + await GenOutboundMux(node, outbound); + + break; + //h2 + case nameof(ETransport.h2): + HttpSettings4Ray httpSettings = new(); + + if (host.IsNotEmpty()) + { + httpSettings.host = Utils.String2List(host); + } + httpSettings.path = path; + + streamSettings.httpSettings = httpSettings; + + break; + //quic + case nameof(ETransport.quic): + QuicSettings4Ray quicsettings = new() + { + security = host, + key = path, + header = new Header4Ray + { + type = node.HeaderType + } + }; + streamSettings.quicSettings = quicsettings; + if (node.StreamSecurity == Global.StreamSecurity) + { + if (sni.IsNotEmpty()) + { + streamSettings.tlsSettings.serverName = sni; + } + else + { + streamSettings.tlsSettings.serverName = node.Address; + } + } + break; + + case nameof(ETransport.grpc): + GrpcSettings4Ray grpcSettings = new() + { + authority = host.IsNullOrEmpty() ? null : host, + serviceName = path, + multiMode = node.HeaderType == Global.GrpcMultiMode, + idle_timeout = _config.GrpcItem.IdleTimeout, + health_check_timeout = _config.GrpcItem.HealthCheckTimeout, + permit_without_stream = _config.GrpcItem.PermitWithoutStream, + initial_windows_size = _config.GrpcItem.InitialWindowsSize, + }; + streamSettings.grpcSettings = grpcSettings; + break; + + default: + //tcp + if (node.HeaderType == Global.TcpHeaderHttp) + { + TcpSettings4Ray tcpSettings = new() + { + header = new Header4Ray + { + type = node.HeaderType + } + }; + + //request Host + string request = EmbedUtils.GetEmbedText(Global.V2raySampleHttpRequestFileName); + string[] arrHost = host.Split(','); + string host2 = string.Join(",".AppendQuotes(), arrHost); + request = request.Replace("$requestHost$", $"{host2.AppendQuotes()}"); + request = request.Replace("$requestUserAgent$", $"{useragent.AppendQuotes()}"); + //Path + string pathHttp = @"/"; + if (path.IsNotEmpty()) + { + string[] arrPath = path.Split(','); + pathHttp = string.Join(",".AppendQuotes(), arrPath); + } + request = request.Replace("$requestPath$", $"{pathHttp.AppendQuotes()}"); + tcpSettings.header.request = JsonUtils.Deserialize(request); + + streamSettings.tcpSettings = tcpSettings; + } + break; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenMoreOutbounds(ProfileItem node, V2rayConfig v2rayConfig) + { + //fragment proxy + if (_config.CoreBasicItem.EnableFragment + && v2rayConfig.outbounds.First().streamSettings?.security.IsNullOrEmpty() == false) + { + var fragmentOutbound = new Outbounds4Ray + { + protocol = "freedom", + tag = $"{Global.ProxyTag}3", + settings = new() + { + fragment = new() + { + packets = _config.Fragment4RayItem?.Packets, + length = _config.Fragment4RayItem?.Length, + interval = _config.Fragment4RayItem?.Interval + } + } + }; + + v2rayConfig.outbounds.Add(fragmentOutbound); + v2rayConfig.outbounds.First().streamSettings.sockopt = new() + { + dialerProxy = fragmentOutbound.tag + }; + return 0; + } + + if (node.Subid.IsNullOrEmpty()) + { + return 0; + } + try + { + var subItem = await AppManager.Instance.GetSubItem(node.Subid); + if (subItem is null) + { + return 0; + } + + //current proxy + var outbound = v2rayConfig.outbounds.First(); + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + + //Previous proxy + var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + string? prevOutboundTag = null; + if (prevNode is not null + && Global.XraySupportConfigType.Contains(prevNode.ConfigType)) + { + var prevOutbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(prevNode, prevOutbound); + prevOutboundTag = $"prev-{Global.ProxyTag}"; + prevOutbound.tag = prevOutboundTag; + v2rayConfig.outbounds.Add(prevOutbound); + } + var nextOutbound = await GenChainOutbounds(subItem, outbound, prevOutboundTag); + + if (nextOutbound is not null) + { + v2rayConfig.outbounds.Insert(0, nextOutbound); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + + return 0; + } + + private async Task GenOutboundsList(List nodes, V2rayConfig v2rayConfig) + { + try + { + // Get template and initialize list + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + if (txtOutbound.IsNullOrEmpty()) + { + return 0; + } + + var resultOutbounds = new List(); + var prevOutbounds = new List(); // Separate list for prev outbounds and fragment + + // Cache for chain proxies to avoid duplicate generation + var nextProxyCache = new Dictionary(); + var prevProxyTags = new Dictionary(); // Map from profile name to tag + int prevIndex = 0; // Index for prev outbounds + + // Process nodes + int index = 0; + foreach (var node in nodes) + { + index++; + + // Handle proxy chain + string? prevTag = null; + var currentOutbound = JsonUtils.Deserialize(txtOutbound); + var nextOutbound = nextProxyCache.GetValueOrDefault(node.Subid, null); + if (nextOutbound != null) + { + nextOutbound = JsonUtils.DeepCopy(nextOutbound); + } + + var subItem = await AppManager.Instance.GetSubItem(node.Subid); + + // current proxy + await GenOutbound(node, currentOutbound); + currentOutbound.tag = $"{Global.ProxyTag}-{index}"; + + if (!node.Subid.IsNullOrEmpty()) + { + if (prevProxyTags.TryGetValue(node.Subid, out var value)) + { + prevTag = value; // maybe null + } + else + { + var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + if (prevNode is not null + && Global.XraySupportConfigType.Contains(prevNode.ConfigType)) + { + var prevOutbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(prevNode, prevOutbound); + prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}"; + prevOutbound.tag = prevTag; + prevOutbounds.Add(prevOutbound); + } + prevProxyTags[node.Subid] = prevTag; + } + + nextOutbound = await GenChainOutbounds(subItem, currentOutbound, prevTag, nextOutbound); + if (!nextProxyCache.ContainsKey(node.Subid)) + { + nextProxyCache[node.Subid] = nextOutbound; + } + } + + if (nextOutbound is not null) + { + resultOutbounds.Add(nextOutbound); + } + resultOutbounds.Add(currentOutbound); + } + + // Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds + resultOutbounds.AddRange(prevOutbounds); + resultOutbounds.AddRange(v2rayConfig.outbounds); + v2rayConfig.outbounds = resultOutbounds; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + + return 0; + } + + /// + /// Generates a chained outbound configuration for the given subItem and outbound. + /// The outbound's tag must be set before calling this method. + /// Returns the next proxy's outbound configuration, which may be null if no next proxy exists. + /// + /// The subscription item containing proxy chain information. + /// The current outbound configuration. Its tag must be set before calling this method. + /// The tag of the previous outbound in the chain, if any. + /// The outbound for the next proxy in the chain, if already created. If null, will be created inside. + /// + /// The outbound configuration for the next proxy in the chain, or null if no next proxy exists. + /// + private async Task GenChainOutbounds(SubItem subItem, Outbounds4Ray outbound, string? prevOutboundTag, Outbounds4Ray? nextOutbound = null) + { + try + { + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + + if (!prevOutboundTag.IsNullOrEmpty()) + { + outbound.streamSettings.sockopt = new() + { + dialerProxy = prevOutboundTag + }; + } + + // Next proxy + var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); + if (nextNode is not null + && Global.XraySupportConfigType.Contains(nextNode.ConfigType)) + { + if (nextOutbound == null) + { + nextOutbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(nextNode, nextOutbound); + } + nextOutbound.tag = outbound.tag; + + outbound.tag = $"mid-{outbound.tag}"; + nextOutbound.streamSettings.sockopt = new() + { + dialerProxy = outbound.tag + }; + } + return nextOutbound; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return null; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs new file mode 100644 index 00000000..1cb46bf2 --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs @@ -0,0 +1,142 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task GenRouting(V2rayConfig v2rayConfig) + { + try + { + if (v2rayConfig.routing?.rules != null) + { + v2rayConfig.routing.domainStrategy = _config.RoutingBasicItem.DomainStrategy; + + var routing = await ConfigHandler.GetDefaultRouting(_config); + if (routing != null) + { + if (routing.DomainStrategy.IsNotEmpty()) + { + v2rayConfig.routing.domainStrategy = routing.DomainStrategy; + } + var rules = JsonUtils.Deserialize>(routing.RuleSet); + foreach (var item in rules) + { + if (item.Enabled) + { + var item2 = JsonUtils.Deserialize(JsonUtils.Serialize(item)); + await GenRoutingUserRule(item2, v2rayConfig); + } + } + } + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return 0; + } + + private async Task GenRoutingUserRule(RulesItem4Ray? rule, V2rayConfig v2rayConfig) + { + try + { + if (rule == null) + { + return 0; + } + rule.outboundTag = await GenRoutingUserRuleOutbound(rule.outboundTag, v2rayConfig); + + if (rule.port.IsNullOrEmpty()) + { + rule.port = null; + } + if (rule.network.IsNullOrEmpty()) + { + rule.network = null; + } + if (rule.domain?.Count == 0) + { + rule.domain = null; + } + if (rule.ip?.Count == 0) + { + rule.ip = null; + } + if (rule.protocol?.Count == 0) + { + rule.protocol = null; + } + if (rule.inboundTag?.Count == 0) + { + rule.inboundTag = null; + } + + var hasDomainIp = false; + if (rule.domain?.Count > 0) + { + var it = JsonUtils.DeepCopy(rule); + it.ip = null; + it.type = "field"; + for (var k = it.domain.Count - 1; k >= 0; k--) + { + if (it.domain[k].StartsWith("#")) + { + it.domain.RemoveAt(k); + } + it.domain[k] = it.domain[k].Replace(Global.RoutingRuleComma, ","); + } + v2rayConfig.routing.rules.Add(it); + hasDomainIp = true; + } + if (rule.ip?.Count > 0) + { + var it = JsonUtils.DeepCopy(rule); + it.domain = null; + it.type = "field"; + v2rayConfig.routing.rules.Add(it); + hasDomainIp = true; + } + if (!hasDomainIp) + { + if (rule.port.IsNotEmpty() + || rule.protocol?.Count > 0 + || rule.inboundTag?.Count > 0 + || rule.network != null + ) + { + var it = JsonUtils.DeepCopy(rule); + it.type = "field"; + v2rayConfig.routing.rules.Add(it); + } + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + + private async Task GenRoutingUserRuleOutbound(string outboundTag, V2rayConfig v2rayConfig) + { + if (Global.OutboundTags.Contains(outboundTag)) + { + return outboundTag; + } + + var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); + if (node == null + || !Global.XraySupportConfigType.Contains(node.ConfigType)) + { + return Global.ProxyTag; + } + + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + var outbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(node, outbound); + outbound.tag = Global.ProxyTag + node.IndexId.ToString(); + v2rayConfig.outbounds.Add(outbound); + + return outbound.tag; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs new file mode 100644 index 00000000..1269a11f --- /dev/null +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs @@ -0,0 +1,51 @@ +namespace ServiceLib.Services.CoreConfig; + +public partial class CoreConfigV2rayService +{ + private async Task GenStatistic(V2rayConfig v2rayConfig) + { + if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed) + { + string tag = EInboundProtocol.api.ToString(); + Metrics4Ray apiObj = new(); + Policy4Ray policyObj = new(); + SystemPolicy4Ray policySystemSetting = new(); + + v2rayConfig.stats = new Stats4Ray(); + + apiObj.tag = tag; + v2rayConfig.metrics = apiObj; + + policySystemSetting.statsOutboundDownlink = true; + policySystemSetting.statsOutboundUplink = true; + policyObj.system = policySystemSetting; + v2rayConfig.policy = policyObj; + + if (!v2rayConfig.inbounds.Exists(item => item.tag == tag)) + { + Inbounds4Ray apiInbound = new(); + Inboundsettings4Ray apiInboundSettings = new(); + apiInbound.tag = tag; + apiInbound.listen = Global.Loopback; + apiInbound.port = AppManager.Instance.StatePort; + apiInbound.protocol = Global.InboundAPIProtocol; + apiInboundSettings.address = Global.Loopback; + apiInbound.settings = apiInboundSettings; + v2rayConfig.inbounds.Add(apiInbound); + } + + if (!v2rayConfig.routing.rules.Exists(item => item.outboundTag == tag)) + { + RulesItem4Ray apiRoutingRule = new() + { + inboundTag = new List { tag }, + outboundTag = tag, + type = "field" + }; + + v2rayConfig.routing.rules.Add(apiRoutingRule); + } + } + return await Task.FromResult(0); + } +} diff --git a/v2rayN/ServiceLib/Services/DownloadService.cs b/v2rayN/ServiceLib/Services/DownloadService.cs new file mode 100644 index 00000000..99170646 --- /dev/null +++ b/v2rayN/ServiceLib/Services/DownloadService.cs @@ -0,0 +1,254 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Sockets; + +namespace ServiceLib.Services; + +/// +///Download +/// +public class DownloadService +{ + public event EventHandler? UpdateCompleted; + + public event ErrorEventHandler? Error; + + private static readonly string _tag = "DownloadService"; + + public async Task DownloadDataAsync(string url, WebProxy webProxy, int downloadTimeout, Func updateFunc) + { + try + { + SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); + + var progress = new Progress(); + progress.ProgressChanged += (sender, value) => updateFunc?.Invoke(false, $"{value}"); + + await DownloaderHelper.Instance.DownloadDataAsync4Speed(webProxy, + url, + progress, + downloadTimeout); + } + catch (Exception ex) + { + await updateFunc?.Invoke(false, ex.Message); + if (ex.InnerException != null) + { + await updateFunc?.Invoke(false, ex.InnerException.Message); + } + } + return 0; + } + + public async Task DownloadFileAsync(string url, string fileName, bool blProxy, int downloadTimeout) + { + try + { + SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); + UpdateCompleted?.Invoke(this, new RetResult(false, $"{ResUI.Downloading} {url}")); + + var progress = new Progress(); + progress.ProgressChanged += (sender, value) => UpdateCompleted?.Invoke(this, new RetResult(value > 100, $"...{value}%")); + + var webProxy = await GetWebProxy(blProxy); + await DownloaderHelper.Instance.DownloadFileAsync(webProxy, + url, + fileName, + progress, + downloadTimeout); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + + Error?.Invoke(this, new ErrorEventArgs(ex)); + if (ex.InnerException != null) + { + Error?.Invoke(this, new ErrorEventArgs(ex.InnerException)); + } + } + } + + public async Task UrlRedirectAsync(string url, bool blProxy) + { + SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); + var webRequestHandler = new SocketsHttpHandler + { + AllowAutoRedirect = false, + Proxy = await GetWebProxy(blProxy) + }; + HttpClient client = new(webRequestHandler); + + var response = await client.GetAsync(url); + if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null) + { + return response.Headers.Location.ToString(); + } + else + { + Error?.Invoke(this, new ErrorEventArgs(new Exception("StatusCode error: " + response.StatusCode))); + Logging.SaveLog("StatusCode error: " + url); + return null; + } + } + + public async Task TryDownloadString(string url, bool blProxy, string userAgent) + { + try + { + var result1 = await DownloadStringAsync(url, blProxy, userAgent, 15); + if (result1.IsNotEmpty()) + { + return result1; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + Error?.Invoke(this, new ErrorEventArgs(ex)); + if (ex.InnerException != null) + { + Error?.Invoke(this, new ErrorEventArgs(ex.InnerException)); + } + } + + try + { + var result2 = await DownloadStringViaDownloader(url, blProxy, userAgent, 15); + if (result2.IsNotEmpty()) + { + return result2; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + Error?.Invoke(this, new ErrorEventArgs(ex)); + if (ex.InnerException != null) + { + Error?.Invoke(this, new ErrorEventArgs(ex.InnerException)); + } + } + + return null; + } + + /// + /// DownloadString + /// + /// + private async Task DownloadStringAsync(string url, bool blProxy, string userAgent, int timeout) + { + try + { + SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); + var webProxy = await GetWebProxy(blProxy); + var client = new HttpClient(new SocketsHttpHandler() + { + Proxy = webProxy, + UseProxy = webProxy != null + }); + + if (userAgent.IsNullOrEmpty()) + { + userAgent = Utils.GetVersion(false); + } + client.DefaultRequestHeaders.UserAgent.TryParseAdd(userAgent); + + Uri uri = new(url); + //Authorization Header + if (uri.UserInfo.IsNotEmpty()) + { + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Utils.Base64Encode(uri.UserInfo)); + } + + using var cts = new CancellationTokenSource(); + var result = await HttpClientHelper.Instance.GetAsync(client, url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token); + return result; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + Error?.Invoke(this, new ErrorEventArgs(ex)); + if (ex.InnerException != null) + { + Error?.Invoke(this, new ErrorEventArgs(ex.InnerException)); + } + } + return null; + } + + /// + /// DownloadString + /// + /// + private async Task DownloadStringViaDownloader(string url, bool blProxy, string userAgent, int timeout) + { + try + { + SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); + + var webProxy = await GetWebProxy(blProxy); + + if (userAgent.IsNullOrEmpty()) + { + userAgent = Utils.GetVersion(false); + } + var result = await DownloaderHelper.Instance.DownloadStringAsync(webProxy, url, userAgent, timeout); + return result; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + Error?.Invoke(this, new ErrorEventArgs(ex)); + if (ex.InnerException != null) + { + Error?.Invoke(this, new ErrorEventArgs(ex.InnerException)); + } + } + return null; + } + + private async Task GetWebProxy(bool blProxy) + { + if (!blProxy) + { + return null; + } + var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); + if (await SocketCheck(Global.Loopback, port) == false) + { + return null; + } + + return new WebProxy($"socks5://{Global.Loopback}:{port}"); + } + + private async Task SocketCheck(string ip, int port) + { + try + { + IPEndPoint point = new(IPAddress.Parse(ip), port); + using Socket? sock = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + await sock.ConnectAsync(point); + return true; + } + catch (Exception) + { + return false; + } + } + + private static void SetSecurityProtocol(bool enableSecurityProtocolTls13) + { + if (enableSecurityProtocolTls13) + { + ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13; + } + else + { + ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; + } + ServicePointManager.DefaultConnectionLimit = 256; + } +} diff --git a/v2rayN/ServiceLib/Services/SpeedtestService.cs b/v2rayN/ServiceLib/Services/SpeedtestService.cs new file mode 100644 index 00000000..5266f4d0 --- /dev/null +++ b/v2rayN/ServiceLib/Services/SpeedtestService.cs @@ -0,0 +1,376 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; + +namespace ServiceLib.Services; + +public class SpeedtestService(Config config, Func updateFunc) +{ + private static readonly string _tag = "SpeedtestService"; + private readonly Config? _config = config; + private readonly Func? _updateFunc = updateFunc; + private static readonly ConcurrentBag _lstExitLoop = new(); + + public void RunLoop(ESpeedActionType actionType, List selecteds) + { + Task.Run(async () => + { + await RunAsync(actionType, selecteds); + await ProfileExManager.Instance.SaveTo(); + await UpdateFunc("", ResUI.SpeedtestingCompleted); + }); + } + + public void ExitLoop() + { + if (_lstExitLoop.Count > 0) + { + UpdateFunc("", ResUI.SpeedtestingStop); + + _lstExitLoop.Clear(); + } + } + + private async Task RunAsync(ESpeedActionType actionType, List selecteds) + { + var exitLoopKey = Utils.GetGuid(false); + _lstExitLoop.Add(exitLoopKey); + + var lstSelected = await GetClearItem(actionType, selecteds); + + switch (actionType) + { + case ESpeedActionType.Tcping: + await RunTcpingAsync(lstSelected); + break; + + case ESpeedActionType.Realping: + await RunRealPingBatchAsync(lstSelected, exitLoopKey); + break; + + case ESpeedActionType.Speedtest: + await RunMixedTestAsync(lstSelected, 1, true, exitLoopKey); + break; + + case ESpeedActionType.Mixedtest: + await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, true, exitLoopKey); + break; + } + } + + private async Task> GetClearItem(ESpeedActionType actionType, List selecteds) + { + var lstSelected = new List(); + foreach (var it in selecteds) + { + if (it.ConfigType == EConfigType.Custom) + { + continue; + } + + if (it.Port <= 0) + { + continue; + } + + lstSelected.Add(new ServerTestItem() + { + IndexId = it.IndexId, + Address = it.Address, + Port = it.Port, + ConfigType = it.ConfigType, + QueueNum = selecteds.IndexOf(it) + }); + } + + //clear test result + foreach (var it in lstSelected) + { + switch (actionType) + { + case ESpeedActionType.Tcping: + case ESpeedActionType.Realping: + await UpdateFunc(it.IndexId, ResUI.Speedtesting, ""); + ProfileExManager.Instance.SetTestDelay(it.IndexId, 0); + break; + + case ESpeedActionType.Speedtest: + await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingWait); + ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0); + break; + + case ESpeedActionType.Mixedtest: + await UpdateFunc(it.IndexId, ResUI.Speedtesting, ResUI.SpeedtestingWait); + ProfileExManager.Instance.SetTestDelay(it.IndexId, 0); + ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0); + break; + } + } + + return lstSelected; + } + + private async Task RunTcpingAsync(List selecteds) + { + List tasks = []; + foreach (var it in selecteds) + { + if (it.ConfigType == EConfigType.Custom) + { + continue; + } + tasks.Add(Task.Run(async () => + { + try + { + var responseTime = await GetTcpingTime(it.Address, it.Port); + + ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); + await UpdateFunc(it.IndexId, responseTime.ToString()); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + })); + } + await Task.WhenAll(tasks); + } + + private async Task RunRealPingBatchAsync(List lstSelected, string exitLoopKey, int pageSize = 0) + { + if (pageSize <= 0) + { + pageSize = lstSelected.Count < Global.SpeedTestPageSize ? lstSelected.Count : Global.SpeedTestPageSize; + } + var lstTest = GetTestBatchItem(lstSelected, pageSize); + + List lstFailed = new(); + foreach (var lst in lstTest) + { + var ret = await RunRealPingAsync(lst, exitLoopKey); + if (ret == false) + { + lstFailed.AddRange(lst); + } + await Task.Delay(100); + } + + //Retest the failed part + var pageSizeNext = pageSize / 2; + if (lstFailed.Count > 0 && pageSizeNext > 0) + { + if (_lstExitLoop.Any(p => p == exitLoopKey) == false) + { + await UpdateFunc("", ResUI.SpeedtestingSkip); + return; + } + + await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count)); + + if (pageSizeNext > _config.SpeedTestItem.MixedConcurrencyCount) + { + await RunRealPingBatchAsync(lstFailed, exitLoopKey, pageSizeNext); + } + else + { + await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, false, exitLoopKey); + } + } + } + + private async Task RunRealPingAsync(List selecteds, string exitLoopKey) + { + var pid = -1; + try + { + pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds); + if (pid < 0) + { + return false; + } + await Task.Delay(1000); + + List tasks = new(); + foreach (var it in selecteds) + { + if (!it.AllowTest) + { + continue; + } + if (it.ConfigType == EConfigType.Custom) + { + continue; + } + tasks.Add(Task.Run(async () => + { + await DoRealPing(it); + })); + } + await Task.WhenAll(tasks); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + finally + { + if (pid > 0) + { + await ProcUtils.ProcessKill(pid); + } + } + return true; + } + + private async Task RunMixedTestAsync(List selecteds, int concurrencyCount, bool blSpeedTest, string exitLoopKey) + { + using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount); + var downloadHandle = new DownloadService(); + List tasks = new(); + foreach (var it in selecteds) + { + if (_lstExitLoop.Any(p => p == exitLoopKey) == false) + { + await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); + continue; + } + if (it.ConfigType == EConfigType.Custom) + { + continue; + } + await concurrencySemaphore.WaitAsync(); + + tasks.Add(Task.Run(async () => + { + var pid = -1; + try + { + pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(it); + if (pid < 0) + { + await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore); + } + else + { + await Task.Delay(1000); + var delay = await DoRealPing(it); + if (blSpeedTest) + { + if (delay > 0) + { + await DoSpeedTest(downloadHandle, it); + } + else + { + await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); + } + } + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + finally + { + if (pid > 0) + { + await ProcUtils.ProcessKill(pid); + } + concurrencySemaphore.Release(); + } + })); + } + await Task.WhenAll(tasks); + } + + private async Task DoRealPing(ServerTestItem it) + { + var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); + var responseTime = await HttpClientHelper.Instance.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); + + ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); + await UpdateFunc(it.IndexId, responseTime.ToString()); + return responseTime; + } + + private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it) + { + await UpdateFunc(it.IndexId, "", ResUI.Speedtesting); + + var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); + var url = _config.SpeedTestItem.SpeedTestUrl; + var timeout = _config.SpeedTestItem.SpeedTestTimeout; + await downloadHandle.DownloadDataAsync(url, webProxy, timeout, async (success, msg) => + { + decimal.TryParse(msg, out var dec); + if (dec > 0) + { + ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec); + } + await UpdateFunc(it.IndexId, "", msg); + }); + } + + private async Task GetTcpingTime(string url, int port) + { + var responseTime = -1; + + try + { + if (!IPAddress.TryParse(url, out var ipAddress)) + { + var ipHostInfo = await Dns.GetHostEntryAsync(url); + ipAddress = ipHostInfo.AddressList.First(); + } + + IPEndPoint endPoint = new(ipAddress, port); + using Socket clientSocket = new(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + var timer = Stopwatch.StartNew(); + var result = clientSocket.BeginConnect(endPoint, null, null); + if (!result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(5))) + { + throw new TimeoutException("connect timeout (5s): " + url); + } + timer.Stop(); + responseTime = (int)timer.Elapsed.TotalMilliseconds; + + clientSocket.EndConnect(result); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return responseTime; + } + + private List> GetTestBatchItem(List lstSelected, int pageSize) + { + List> lstTest = new(); + var lst1 = lstSelected.Where(t => Global.XraySupportConfigType.Contains(t.ConfigType)).ToList(); + var lst2 = lstSelected.Where(t => Global.SingboxSupportConfigType.Contains(t.ConfigType) && !Global.XraySupportConfigType.Contains(t.ConfigType)).ToList(); + + for (var num = 0; num < (int)Math.Ceiling(lst1.Count * 1.0 / pageSize); num++) + { + lstTest.Add(lst1.Skip(num * pageSize).Take(pageSize).ToList()); + } + for (var num = 0; num < (int)Math.Ceiling(lst2.Count * 1.0 / pageSize); num++) + { + lstTest.Add(lst2.Skip(num * pageSize).Take(pageSize).ToList()); + } + + return lstTest; + } + + private async Task UpdateFunc(string indexId, string delay, string speed = "") + { + await _updateFunc?.Invoke(new() { IndexId = indexId, Delay = delay, Speed = speed }); + if (indexId.IsNotEmpty() && speed.IsNotEmpty()) + { + ProfileExManager.Instance.SetTestMessage(indexId, speed); + } + } +} diff --git a/v2rayN/ServiceLib/Services/Statistics/StatisticsSingboxService.cs b/v2rayN/ServiceLib/Services/Statistics/StatisticsSingboxService.cs new file mode 100644 index 00000000..d3de6a18 --- /dev/null +++ b/v2rayN/ServiceLib/Services/Statistics/StatisticsSingboxService.cs @@ -0,0 +1,126 @@ +using System.Net.WebSockets; +using System.Text; + +namespace ServiceLib.Services.Statistics; + +public class StatisticsSingboxService +{ + private readonly Config _config; + private bool _exitFlag; + private ClientWebSocket? webSocket; + private readonly Func? _updateFunc; + private string Url => $"ws://{Global.Loopback}:{AppManager.Instance.StatePort2}/traffic"; + private static readonly string _tag = "StatisticsSingboxService"; + + public StatisticsSingboxService(Config config, Func updateFunc) + { + _config = config; + _updateFunc = updateFunc; + _exitFlag = false; + + _ = Task.Run(Run); + } + + private async Task Init() + { + await Task.Delay(5000); + + try + { + if (webSocket == null) + { + webSocket = new ClientWebSocket(); + await webSocket.ConnectAsync(new Uri(Url), CancellationToken.None); + } + } + catch { } + } + + public void Close() + { + try + { + _exitFlag = true; + if (webSocket != null) + { + webSocket.Abort(); + webSocket = null; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + + private async Task Run() + { + await Init(); + + while (!_exitFlag) + { + await Task.Delay(1000); + try + { + if (!_config.IsRunningCore(ECoreType.sing_box)) + { + continue; + } + if (webSocket != null) + { + if (webSocket.State is WebSocketState.Aborted or WebSocketState.Closed) + { + webSocket.Abort(); + webSocket = null; + await Init(); + continue; + } + + if (webSocket.State != WebSocketState.Open) + { + continue; + } + + var buffer = new byte[1024]; + var res = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + while (!res.CloseStatus.HasValue) + { + var result = Encoding.UTF8.GetString(buffer, 0, res.Count); + if (result.IsNotEmpty()) + { + ParseOutput(result, out var up, out var down); + + await _updateFunc?.Invoke(new ServerSpeedItem() + { + ProxyUp = (long)(up / 1000), + ProxyDown = (long)(down / 1000) + }); + } + res = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + } + } + catch + { + } + } + } + + private void ParseOutput(string source, out ulong up, out ulong down) + { + up = 0; + down = 0; + try + { + var trafficItem = JsonUtils.Deserialize(source); + if (trafficItem != null) + { + up = trafficItem.Up; + down = trafficItem.Down; + } + } + catch + { + } + } +} diff --git a/v2rayN/ServiceLib/Services/Statistics/StatisticsXrayService.cs b/v2rayN/ServiceLib/Services/Statistics/StatisticsXrayService.cs new file mode 100644 index 00000000..b64a515d --- /dev/null +++ b/v2rayN/ServiceLib/Services/Statistics/StatisticsXrayService.cs @@ -0,0 +1,107 @@ +namespace ServiceLib.Services.Statistics; + +public class StatisticsXrayService +{ + private const long linkBase = 1024; + private ServerSpeedItem _serverSpeedItem = new(); + private readonly Config _config; + private bool _exitFlag; + private readonly Func? _updateFunc; + private string Url => $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort}/debug/vars"; + + public StatisticsXrayService(Config config, Func updateFunc) + { + _config = config; + _updateFunc = updateFunc; + _exitFlag = false; + + _ = Task.Run(Run); + } + + public void Close() + { + _exitFlag = true; + } + + private async Task Run() + { + while (!_exitFlag) + { + await Task.Delay(1000); + try + { + if (_config.RunningCoreType != ECoreType.Xray) + { + continue; + } + + var result = await HttpClientHelper.Instance.TryGetAsync(Url); + if (result != null) + { + var server = ParseOutput(result) ?? new ServerSpeedItem(); + await _updateFunc?.Invoke(server); + } + } + catch + { + // ignored + } + } + } + + private ServerSpeedItem? ParseOutput(string result) + { + try + { + var source = JsonUtils.Deserialize(result); + if (source?.stats?.outbound == null) + { + return null; + } + + ServerSpeedItem server = new(); + foreach (var key in source.stats.outbound.Keys.Cast()) + { + var value = source.stats.outbound[key]; + if (value == null) + { + continue; + } + var state = JsonUtils.Deserialize(value.ToString()); + + if (key.StartsWith(Global.ProxyTag)) + { + server.ProxyUp += state.uplink / linkBase; + server.ProxyDown += state.downlink / linkBase; + } + else if (key == Global.DirectTag) + { + server.DirectUp = state.uplink / linkBase; + server.DirectDown = state.downlink / linkBase; + } + } + + if (server.DirectDown < _serverSpeedItem.DirectDown || server.ProxyDown < _serverSpeedItem.ProxyDown) + { + _serverSpeedItem = new(); + return null; + } + + ServerSpeedItem curItem = new() + { + ProxyUp = server.ProxyUp - _serverSpeedItem.ProxyUp, + ProxyDown = server.ProxyDown - _serverSpeedItem.ProxyDown, + DirectUp = server.DirectUp - _serverSpeedItem.DirectUp, + DirectDown = server.DirectDown - _serverSpeedItem.DirectDown, + }; + _serverSpeedItem = server; + return curItem; + } + catch + { + // ignored + } + + return null; + } +} diff --git a/v2rayN/ServiceLib/Services/UpdateService.cs b/v2rayN/ServiceLib/Services/UpdateService.cs new file mode 100644 index 00000000..429c0a62 --- /dev/null +++ b/v2rayN/ServiceLib/Services/UpdateService.cs @@ -0,0 +1,487 @@ +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace ServiceLib.Services; + +public class UpdateService +{ + private Func? _updateFunc; + private readonly int _timeout = 30; + private static readonly string _tag = "UpdateService"; + + public async Task CheckUpdateGuiN(Config config, Func updateFunc, bool preRelease) + { + _updateFunc = updateFunc; + var url = string.Empty; + var fileName = string.Empty; + + DownloadService downloadHandle = new(); + downloadHandle.UpdateCompleted += (sender2, args) => + { + if (args.Success) + { + UpdateFunc(false, ResUI.MsgDownloadV2rayCoreSuccessfully); + UpdateFunc(true, Utils.UrlEncode(fileName)); + } + else + { + UpdateFunc(false, args.Msg); + } + }; + downloadHandle.Error += (sender2, args) => + { + UpdateFunc(false, args.GetException().Message); + }; + + await UpdateFunc(false, string.Format(ResUI.MsgStartUpdating, ECoreType.v2rayN)); + var result = await CheckUpdateAsync(downloadHandle, ECoreType.v2rayN, preRelease); + if (result.Success) + { + await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, ECoreType.v2rayN)); + await UpdateFunc(false, result.Msg); + + url = result.Data?.ToString(); + fileName = Utils.GetTempPath(Utils.GetGuid()); + await downloadHandle.DownloadFileAsync(url, fileName, true, _timeout); + } + else + { + await UpdateFunc(false, result.Msg); + } + } + + public async Task CheckUpdateCore(ECoreType type, Config config, Func updateFunc, bool preRelease) + { + _updateFunc = updateFunc; + var url = string.Empty; + var fileName = string.Empty; + + DownloadService downloadHandle = new(); + downloadHandle.UpdateCompleted += (sender2, args) => + { + if (args.Success) + { + UpdateFunc(false, ResUI.MsgDownloadV2rayCoreSuccessfully); + UpdateFunc(false, ResUI.MsgUnpacking); + + try + { + UpdateFunc(true, fileName); + } + catch (Exception ex) + { + UpdateFunc(false, ex.Message); + } + } + else + { + UpdateFunc(false, args.Msg); + } + }; + downloadHandle.Error += (sender2, args) => + { + UpdateFunc(false, args.GetException().Message); + }; + + await UpdateFunc(false, string.Format(ResUI.MsgStartUpdating, type)); + var result = await CheckUpdateAsync(downloadHandle, type, preRelease); + if (result.Success) + { + await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, type)); + await UpdateFunc(false, result.Msg); + + url = result.Data?.ToString(); + var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url); + fileName = Utils.GetTempPath(Utils.GetGuid() + ext); + await downloadHandle.DownloadFileAsync(url, fileName, true, _timeout); + } + else + { + if (!result.Msg.IsNullOrEmpty()) + { + await UpdateFunc(false, result.Msg); + } + } + } + + public async Task UpdateGeoFileAll(Config config, Func updateFunc) + { + await UpdateGeoFiles(config, updateFunc); + await UpdateOtherFiles(config, updateFunc); + await UpdateSrsFileAll(config, updateFunc); + await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo")); + } + + #region CheckUpdate private + + private async Task CheckUpdateAsync(DownloadService downloadHandle, ECoreType type, bool preRelease) + { + try + { + var result = await GetRemoteVersion(downloadHandle, type, preRelease); + if (!result.Success || result.Data is null) + { + return result; + } + return await ParseDownloadUrl(type, (SemanticVersion)result.Data); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + await UpdateFunc(false, ex.Message); + return new RetResult(false, ex.Message); + } + } + + private async Task GetRemoteVersion(DownloadService downloadHandle, ECoreType type, bool preRelease) + { + var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); + var tagName = string.Empty; + if (preRelease) + { + var url = coreInfo?.ReleaseApiUrl; + var result = await downloadHandle.TryDownloadString(url, true, Global.AppName); + if (result.IsNullOrEmpty()) + { + return new RetResult(false, ""); + } + + var gitHubReleases = JsonUtils.Deserialize>(result); + var gitHubRelease = preRelease ? gitHubReleases?.First() : gitHubReleases?.First(r => r.Prerelease == false); + tagName = gitHubRelease?.TagName; + //var body = gitHubRelease?.Body; + } + else + { + var url = Path.Combine(coreInfo.Url, "latest"); + var lastUrl = await downloadHandle.UrlRedirectAsync(url, true); + if (lastUrl == null) + { + return new RetResult(false, ""); + } + + tagName = lastUrl?.Split("/tag/").LastOrDefault(); + } + return new RetResult(true, "", new SemanticVersion(tagName)); + } + + private async Task GetCoreVersion(ECoreType type) + { + try + { + var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); + string filePath = string.Empty; + foreach (var name in coreInfo.CoreExes) + { + var vName = Utils.GetBinPath(Utils.GetExeName(name), coreInfo.CoreType.ToString()); + if (File.Exists(vName)) + { + filePath = vName; + break; + } + } + + if (!File.Exists(filePath)) + { + string msg = string.Format(ResUI.NotFoundCore, @"", "", ""); + //ShowMsg(true, msg); + return new SemanticVersion(""); + } + + var result = await Utils.GetCliWrapOutput(filePath, coreInfo.VersionArg); + var echo = result ?? ""; + string version = string.Empty; + switch (type) + { + case ECoreType.v2fly: + case ECoreType.Xray: + case ECoreType.v2fly_v5: + version = Regex.Match(echo, $"{coreInfo.Match} ([0-9.]+) \\(").Groups[1].Value; + break; + + case ECoreType.mihomo: + version = Regex.Match(echo, $"v[0-9.]+").Groups[0].Value; + break; + + case ECoreType.sing_box: + version = Regex.Match(echo, $"([0-9.]+)").Groups[1].Value; + break; + } + return new SemanticVersion(version); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + await UpdateFunc(false, ex.Message); + return new SemanticVersion(""); + } + } + + private async Task ParseDownloadUrl(ECoreType type, SemanticVersion version) + { + try + { + var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); + var coreUrl = await GetUrlFromCore(coreInfo) ?? string.Empty; + SemanticVersion curVersion; + string message; + string? url; + switch (type) + { + case ECoreType.v2fly: + case ECoreType.Xray: + case ECoreType.v2fly_v5: + { + curVersion = await GetCoreVersion(type); + message = string.Format(ResUI.IsLatestCore, type, curVersion.ToVersionString("v")); + url = string.Format(coreUrl, version.ToVersionString("v")); + break; + } + case ECoreType.mihomo: + { + curVersion = await GetCoreVersion(type); + message = string.Format(ResUI.IsLatestCore, type, curVersion); + url = string.Format(coreUrl, version.ToVersionString("v")); + break; + } + case ECoreType.sing_box: + { + curVersion = await GetCoreVersion(type); + message = string.Format(ResUI.IsLatestCore, type, curVersion.ToVersionString("v")); + url = string.Format(coreUrl, version.ToVersionString("v"), version); + break; + } + case ECoreType.v2rayN: + { + curVersion = new SemanticVersion(Utils.GetVersionInfo()); + message = string.Format(ResUI.IsLatestN, type, curVersion); + url = string.Format(coreUrl, version); + break; + } + default: + throw new ArgumentException("Type"); + } + + if (curVersion >= version && version != new SemanticVersion(0, 0, 0)) + { + return new RetResult(false, message); + } + + return new RetResult(true, "", url); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + await UpdateFunc(false, ex.Message); + return new RetResult(false, ex.Message); + } + } + + private async Task GetUrlFromCore(CoreInfo? coreInfo) + { + if (Utils.IsWindows()) + { + var url = RuntimeInformation.ProcessArchitecture switch + { + Architecture.Arm64 => coreInfo?.DownloadUrlWinArm64, + Architecture.X64 => coreInfo?.DownloadUrlWin64, + _ => null, + }; + + if (coreInfo?.CoreType != ECoreType.v2rayN) + { + return url; + } + + //Check for standalone windows .Net version + if (File.Exists(Path.Combine(Utils.GetBaseDirectory(), "wpfgfx_cor3.dll")) + && File.Exists(Path.Combine(Utils.GetBaseDirectory(), "D3DCompiler_47_cor3.dll"))) + { + return url?.Replace(".zip", "-SelfContained.zip"); + } + + //Check for avalonia desktop windows version + if (File.Exists(Path.Combine(Utils.GetBaseDirectory(), "libHarfBuzzSharp.dll"))) + { + return url?.Replace(".zip", "-desktop.zip"); + } + + return url; + } + else if (Utils.IsLinux()) + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.Arm64 => coreInfo?.DownloadUrlLinuxArm64, + Architecture.X64 => coreInfo?.DownloadUrlLinux64, + _ => null, + }; + } + else if (Utils.IsOSX()) + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.Arm64 => coreInfo?.DownloadUrlOSXArm64, + Architecture.X64 => coreInfo?.DownloadUrlOSX64, + _ => null, + }; + } + return await Task.FromResult(""); + } + + #endregion CheckUpdate private + + #region Geo private + + private async Task UpdateGeoFiles(Config config, Func updateFunc) + { + _updateFunc = updateFunc; + + var geoUrl = string.IsNullOrEmpty(config?.ConstItem.GeoSourceUrl) + ? Global.GeoUrl + : config.ConstItem.GeoSourceUrl; + + List files = ["geosite", "geoip"]; + foreach (var geoName in files) + { + var fileName = $"{geoName}.dat"; + var targetPath = Utils.GetBinPath($"{fileName}"); + var url = string.Format(geoUrl, geoName); + + await DownloadGeoFile(url, fileName, targetPath, updateFunc); + } + } + + private async Task UpdateOtherFiles(Config config, Func updateFunc) + { + //If it is not in China area, no update is required + if (config.ConstItem.GeoSourceUrl.IsNotEmpty()) + { + return; + } + + _updateFunc = updateFunc; + + foreach (var url in Global.OtherGeoUrls) + { + var fileName = Path.GetFileName(url); + var targetPath = Utils.GetBinPath($"{fileName}"); + + await DownloadGeoFile(url, fileName, targetPath, updateFunc); + } + } + + private async Task UpdateSrsFileAll(Config config, Func updateFunc) + { + _updateFunc = updateFunc; + + var geoipFiles = new List(); + var geoSiteFiles = new List(); + + //Collect used files list + var routingItems = await AppManager.Instance.RoutingItems(); + foreach (var routing in routingItems) + { + var rules = JsonUtils.Deserialize>(routing.RuleSet); + foreach (var item in rules ?? []) + { + foreach (var ip in item.Ip ?? []) + { + var prefix = "geoip:"; + if (ip.StartsWith(prefix)) + { + geoipFiles.Add(ip.Substring(prefix.Length)); + } + } + + foreach (var domain in item.Domain ?? []) + { + var prefix = "geosite:"; + if (domain.StartsWith(prefix)) + { + geoSiteFiles.Add(domain.Substring(prefix.Length)); + } + } + } + } + + //append dns items TODO + geoSiteFiles.Add("cn"); + geoSiteFiles.Add("geolocation-cn"); + geoSiteFiles.Add("category-ads-all"); + + var path = Utils.GetBinPath("srss"); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + foreach (var item in geoipFiles.Distinct()) + { + await UpdateSrsFile("geoip", item, config, updateFunc); + } + + foreach (var item in geoSiteFiles.Distinct()) + { + await UpdateSrsFile("geosite", item, config, updateFunc); + } + } + + private async Task UpdateSrsFile(string type, string srsName, Config config, Func updateFunc) + { + var srsUrl = string.IsNullOrEmpty(config.ConstItem.SrsSourceUrl) + ? Global.SingboxRulesetUrl + : config.ConstItem.SrsSourceUrl; + + var fileName = $"{type}-{srsName}.srs"; + var targetPath = Path.Combine(Utils.GetBinPath("srss"), fileName); + var url = string.Format(srsUrl, type, $"{type}-{srsName}", srsName); + + await DownloadGeoFile(url, fileName, targetPath, updateFunc); + } + + private async Task DownloadGeoFile(string url, string fileName, string targetPath, Func updateFunc) + { + var tmpFileName = Utils.GetTempPath(Utils.GetGuid()); + + DownloadService downloadHandle = new(); + downloadHandle.UpdateCompleted += (sender2, args) => + { + if (args.Success) + { + UpdateFunc(false, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, fileName)); + + try + { + if (File.Exists(tmpFileName)) + { + File.Copy(tmpFileName, targetPath, true); + + File.Delete(tmpFileName); + //await UpdateFunc(true, ""); + } + } + catch (Exception ex) + { + UpdateFunc(false, ex.Message); + } + } + else + { + UpdateFunc(false, args.Msg); + } + }; + downloadHandle.Error += (sender2, args) => + { + UpdateFunc(false, args.GetException().Message); + }; + + await downloadHandle.DownloadFileAsync(url, tmpFileName, true, _timeout); + } + + #endregion Geo private + + private async Task UpdateFunc(bool notify, string msg) + { + await _updateFunc?.Invoke(notify, msg); + } +} diff --git a/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs new file mode 100644 index 00000000..a94ecf74 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs @@ -0,0 +1,115 @@ +using System.Reactive; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class AddServer2ViewModel : MyReactiveObject +{ + [Reactive] + public ProfileItem SelectedSource { get; set; } + + [Reactive] + public string? CoreType { get; set; } + + public ReactiveCommand BrowseServerCmd { get; } + public ReactiveCommand EditServerCmd { get; } + public ReactiveCommand SaveServerCmd { get; } + public bool IsModified { get; set; } + + public AddServer2ViewModel(ProfileItem profileItem, Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + BrowseServerCmd = ReactiveCommand.CreateFromTask(async () => + { + _updateView?.Invoke(EViewAction.BrowseServer, null); + await Task.CompletedTask; + }); + EditServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await EditServer(); + }); + SaveServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveServerAsync(); + }); + + SelectedSource = profileItem.IndexId.IsNullOrEmpty() ? profileItem : JsonUtils.DeepCopy(profileItem); + CoreType = SelectedSource?.CoreType?.ToString(); + } + + private async Task SaveServerAsync() + { + var remarks = SelectedSource.Remarks; + if (remarks.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); + return; + } + + if (SelectedSource.Address.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.FillServerAddressCustom); + return; + } + SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); + + if (await ConfigHandler.EditCustomServer(_config, SelectedSource) == 0) + { + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + _updateView?.Invoke(EViewAction.CloseWindow, null); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } + + public async Task BrowseServer(string fileName) + { + if (fileName.IsNullOrEmpty()) + { + return; + } + + var item = await AppManager.Instance.GetProfileItem(SelectedSource.IndexId); + item ??= SelectedSource; + item.Address = fileName; + if (await ConfigHandler.AddCustomServer(_config, item, false) == 0) + { + NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedCustomServer); + if (item.IndexId.IsNotEmpty()) + { + SelectedSource = JsonUtils.DeepCopy(item); + } + IsModified = true; + } + else + { + NoticeManager.Instance.Enqueue(ResUI.FailedImportedCustomServer); + } + } + + private async Task EditServer() + { + var address = SelectedSource.Address; + if (address.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.FillServerAddressCustom); + return; + } + + address = Utils.GetConfigPath(address); + if (File.Exists(address)) + { + ProcUtils.ProcessStart(address); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.FailedReadConfiguration); + } + await Task.CompletedTask; + } +} diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs new file mode 100644 index 00000000..cd2399bc --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -0,0 +1,95 @@ +using System.Reactive; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class AddServerViewModel : MyReactiveObject +{ + [Reactive] + public ProfileItem SelectedSource { get; set; } + + [Reactive] + public string? CoreType { get; set; } + + public ReactiveCommand SaveCmd { get; } + + public AddServerViewModel(ProfileItem profileItem, Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + SaveCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveServerAsync(); + }); + + if (profileItem.IndexId.IsNullOrEmpty()) + { + profileItem.Network = Global.DefaultNetwork; + profileItem.HeaderType = Global.None; + profileItem.RequestHost = ""; + profileItem.StreamSecurity = ""; + SelectedSource = profileItem; + } + else + { + SelectedSource = JsonUtils.DeepCopy(profileItem); + } + CoreType = SelectedSource?.CoreType?.ToString(); + } + + private async Task SaveServerAsync() + { + if (SelectedSource.Remarks.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); + return; + } + + if (SelectedSource.Address.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.FillServerAddress); + return; + } + var port = SelectedSource.Port.ToString(); + if (port.IsNullOrEmpty() || !Utils.IsNumeric(port) + || SelectedSource.Port <= 0 || SelectedSource.Port >= Global.MaxPort) + { + NoticeManager.Instance.Enqueue(ResUI.FillCorrectServerPort); + return; + } + if (SelectedSource.ConfigType == EConfigType.Shadowsocks) + { + if (SelectedSource.Id.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.FillPassword); + return; + } + if (SelectedSource.Security.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectEncryption); + return; + } + } + if (SelectedSource.ConfigType is not EConfigType.SOCKS and not EConfigType.HTTP) + { + if (SelectedSource.Id.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.FillUUID); + return; + } + } + SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); + + if (await ConfigHandler.AddServer(_config, SelectedSource) == 0) + { + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + _updateView?.Invoke(EViewAction.CloseWindow, null); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } +} diff --git a/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs b/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs new file mode 100644 index 00000000..5a96a04d --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs @@ -0,0 +1,179 @@ +using System.Reactive; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class BackupAndRestoreViewModel : MyReactiveObject +{ + private readonly string _guiConfigs = "guiConfigs"; + private static string BackupFileName => $"backup_{DateTime.Now:yyyyMMddHHmmss}.zip"; + + public ReactiveCommand RemoteBackupCmd { get; } + public ReactiveCommand RemoteRestoreCmd { get; } + public ReactiveCommand WebDavCheckCmd { get; } + + [Reactive] + public WebDavItem SelectedSource { get; set; } + + [Reactive] + public string OperationMsg { get; set; } + + public BackupAndRestoreViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + WebDavCheckCmd = ReactiveCommand.CreateFromTask(async () => + { + await WebDavCheck(); + }); + RemoteBackupCmd = ReactiveCommand.CreateFromTask(async () => + { + await RemoteBackup(); + }); + RemoteRestoreCmd = ReactiveCommand.CreateFromTask(async () => + { + await RemoteRestore(); + }); + + SelectedSource = JsonUtils.DeepCopy(_config.WebDavItem); + } + + private void DisplayOperationMsg(string msg = "") + { + OperationMsg = msg; + } + + private async Task WebDavCheck() + { + DisplayOperationMsg(); + _config.WebDavItem = SelectedSource; + _ = await ConfigHandler.SaveConfig(_config); + + var result = await WebDavManager.Instance.CheckConnection(); + if (result) + { + DisplayOperationMsg(ResUI.OperationSuccess); + } + else + { + DisplayOperationMsg(WebDavManager.Instance.GetLastError()); + } + } + + private async Task RemoteBackup() + { + DisplayOperationMsg(); + var fileName = Utils.GetBackupPath(BackupFileName); + var result = await CreateZipFileFromDirectory(fileName); + if (result) + { + var result2 = await WebDavManager.Instance.PutFile(fileName); + if (result2) + { + DisplayOperationMsg(ResUI.OperationSuccess); + return; + } + } + + DisplayOperationMsg(WebDavManager.Instance.GetLastError()); + } + + private async Task RemoteRestore() + { + DisplayOperationMsg(); + var fileName = Utils.GetTempPath(Utils.GetGuid()); + var result = await WebDavManager.Instance.GetRawFile(fileName); + if (result) + { + await LocalRestore(fileName); + return; + } + + DisplayOperationMsg(WebDavManager.Instance.GetLastError()); + } + + public async Task LocalBackup(string fileName) + { + DisplayOperationMsg(); + var result = await CreateZipFileFromDirectory(fileName); + if (result) + { + DisplayOperationMsg(ResUI.OperationSuccess); + } + else + { + DisplayOperationMsg(WebDavManager.Instance.GetLastError()); + } + + return result; + } + + public async Task LocalRestore(string fileName) + { + DisplayOperationMsg(); + if (fileName.IsNullOrEmpty()) + { + return; + } + //exist + if (!File.Exists(fileName)) + { + return; + } + //check + var lstFiles = FileManager.GetFilesFromZip(fileName); + if (lstFiles is null || !lstFiles.Any(t => t.Contains(_guiConfigs))) + { + DisplayOperationMsg(ResUI.LocalRestoreInvalidZipTips); + return; + } + + //backup first + var fileBackup = Utils.GetBackupPath(BackupFileName); + var result = await CreateZipFileFromDirectory(fileBackup); + if (result) + { + await AppManager.Instance.AppExitAsync(false); + await SQLiteHelper.Instance.DisposeDbConnectionAsync(); + + var toPath = Utils.GetConfigPath(); + FileManager.ZipExtractToFile(fileName, toPath, ""); + + if (Utils.IsWindows()) + { + ProcUtils.RebootAsAdmin(false); + } + else + { + if (Utils.UpgradeAppExists(out var upgradeFileName)) + { + _ = ProcUtils.ProcessStart(upgradeFileName, Global.RebootAs, Utils.StartupPath()); + } + } + AppManager.Instance.Shutdown(true); + } + else + { + DisplayOperationMsg(WebDavManager.Instance.GetLastError()); + } + } + + private async Task CreateZipFileFromDirectory(string fileName) + { + if (fileName.IsNullOrEmpty()) + { + return false; + } + + var configDir = Utils.GetConfigPath(); + var configDirZipTemp = Utils.GetTempPath($"v2rayN_{DateTime.Now:yyyyMMddHHmmss}"); + var configDirTemp = Path.Combine(configDirZipTemp, _guiConfigs); + + FileManager.CopyDirectory(configDir, configDirTemp, false, true, ""); + var ret = FileManager.CreateFromDirectory(configDirZipTemp, fileName); + Directory.Delete(configDirZipTemp, true); + return await Task.FromResult(ret); + } +} diff --git a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs new file mode 100644 index 00000000..23839141 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs @@ -0,0 +1,342 @@ +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Runtime.InteropServices; +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Splat; + +namespace ServiceLib.ViewModels; + +public class CheckUpdateViewModel : MyReactiveObject +{ + private const string _geo = "GeoFiles"; + private readonly string _v2rayN = ECoreType.v2rayN.ToString(); + private List _lstUpdated = []; + private static readonly string _tag = "CheckUpdateViewModel"; + + public IObservableCollection CheckUpdateModels { get; } = new ObservableCollectionExtended(); + public ReactiveCommand CheckUpdateCmd { get; } + [Reactive] public bool EnableCheckPreReleaseUpdate { get; set; } + + public CheckUpdateViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + CheckUpdateCmd = ReactiveCommand.CreateFromTask(CheckUpdate); + CheckUpdateCmd.ThrownExceptions.Subscribe(ex => + { + Logging.SaveLog(_tag, ex); + _ = UpdateView(_v2rayN, ex.Message); + }); + + EnableCheckPreReleaseUpdate = _config.CheckUpdateItem.CheckPreReleaseUpdate; + + this.WhenAnyValue( + x => x.EnableCheckPreReleaseUpdate, + y => y == true) + .Subscribe(c => { _config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate; }); + + RefreshCheckUpdateItems(); + } + + private void RefreshCheckUpdateItems() + { + CheckUpdateModels.Clear(); + + if (RuntimeInformation.ProcessArchitecture != Architecture.X86) + { + CheckUpdateModels.Add(GetCheckUpdateModel(_v2rayN)); + //Not Windows and under Win10 + if (!(Utils.IsWindows() && Environment.OSVersion.Version.Major < 10)) + { + CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.Xray.ToString())); + CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.mihomo.ToString())); + CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.sing_box.ToString())); + } + } + CheckUpdateModels.Add(GetCheckUpdateModel(_geo)); + } + + private CheckUpdateModel GetCheckUpdateModel(string coreType) + { + return new() + { + IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType) ?? true, + CoreType = coreType, + Remarks = ResUI.menuCheckUpdate, + }; + } + + private async Task SaveSelectedCoreTypes() + { + _config.CheckUpdateItem.SelectedCoreTypes = CheckUpdateModels.Where(t => t.IsSelected == true).Select(t => t.CoreType ?? "").ToList(); + await ConfigHandler.SaveConfig(_config); + } + + private async Task CheckUpdate() + { + await Task.Run(CheckUpdateTask); + } + + private async Task CheckUpdateTask() + { + _lstUpdated.Clear(); + _lstUpdated = CheckUpdateModels.Where(x => x.IsSelected == true) + .Select(x => new CheckUpdateModel() { CoreType = x.CoreType }).ToList(); + await SaveSelectedCoreTypes(); + + for (var k = CheckUpdateModels.Count - 1; k >= 0; k--) + { + var item = CheckUpdateModels[k]; + if (item.IsSelected != true) + { + continue; + } + + await UpdateView(item.CoreType, "..."); + if (item.CoreType == _geo) + { + await CheckUpdateGeo(); + } + else if (item.CoreType == _v2rayN) + { + await CheckUpdateN(EnableCheckPreReleaseUpdate); + } + else if (item.CoreType == ECoreType.Xray.ToString()) + { + await CheckUpdateCore(item, EnableCheckPreReleaseUpdate); + } + else + { + await CheckUpdateCore(item, false); + } + } + + await UpdateFinished(); + } + + private void UpdatedPlusPlus(string coreType, string fileName) + { + var item = _lstUpdated.FirstOrDefault(x => x.CoreType == coreType); + if (item == null) + { + return; + } + item.IsFinished = true; + if (!fileName.IsNullOrEmpty()) + { + item.FileName = fileName; + } + } + + private async Task CheckUpdateGeo() + { + async Task _updateUI(bool success, string msg) + { + await UpdateView(_geo, msg); + if (success) + { + UpdatedPlusPlus(_geo, ""); + } + } + await (new UpdateService()).UpdateGeoFileAll(_config, _updateUI) + .ContinueWith(t => + { + UpdatedPlusPlus(_geo, ""); + }); + } + + private async Task CheckUpdateN(bool preRelease) + { + async Task _updateUI(bool success, string msg) + { + await UpdateView(_v2rayN, msg); + if (success) + { + await UpdateView(_v2rayN, ResUI.OperationSuccess); + UpdatedPlusPlus(_v2rayN, msg); + } + } + await (new UpdateService()).CheckUpdateGuiN(_config, _updateUI, preRelease) + .ContinueWith(t => + { + UpdatedPlusPlus(_v2rayN, ""); + }); + } + + private async Task CheckUpdateCore(CheckUpdateModel model, bool preRelease) + { + async Task _updateUI(bool success, string msg) + { + await UpdateView(model.CoreType, msg); + if (success) + { + await UpdateView(model.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfullyMore); + + UpdatedPlusPlus(model.CoreType, msg); + } + } + var type = (ECoreType)Enum.Parse(typeof(ECoreType), model.CoreType); + await (new UpdateService()).CheckUpdateCore(type, _config, _updateUI, preRelease) + .ContinueWith(t => + { + UpdatedPlusPlus(model.CoreType, ""); + }); + } + + private async Task UpdateFinished() + { + if (_lstUpdated.Count > 0 && _lstUpdated.Count(x => x.IsFinished == true) == _lstUpdated.Count) + { + await UpdateFinishedSub(false); + await Task.Delay(2000); + await UpgradeCore(); + + if (_lstUpdated.Any(x => x.CoreType == _v2rayN && x.IsFinished == true)) + { + await Task.Delay(1000); + await UpgradeN(); + } + await Task.Delay(1000); + await UpdateFinishedSub(true); + } + } + + private async Task UpdateFinishedSub(bool blReload) + { + RxApp.MainThreadScheduler.Schedule(blReload, (scheduler, blReload) => + { + _ = UpdateFinishedResult(blReload); + return Disposable.Empty; + }); + } + + public async Task UpdateFinishedResult(bool blReload) + { + if (blReload) + { + Locator.Current.GetService()?.Reload(); + } + else + { + Locator.Current.GetService()?.CloseCore(); + } + } + + private async Task UpgradeN() + { + try + { + var fileName = _lstUpdated.FirstOrDefault(x => x.CoreType == _v2rayN)?.FileName; + if (fileName.IsNullOrEmpty()) + { + return; + } + if (!Utils.UpgradeAppExists(out var upgradeFileName)) + { + await UpdateView(_v2rayN, ResUI.UpgradeAppNotExistTip); + NoticeManager.Instance.SendMessageAndEnqueue(ResUI.UpgradeAppNotExistTip); + Logging.SaveLog("UpgradeApp does not exist"); + return; + } + + var id = ProcUtils.ProcessStart(upgradeFileName, fileName, Utils.StartupPath()); + if (id > 0) + { + await AppManager.Instance.AppExitAsync(true); + } + } + catch (Exception ex) + { + await UpdateView(_v2rayN, ex.Message); + } + } + + private async Task UpgradeCore() + { + foreach (var item in _lstUpdated) + { + if (item.FileName.IsNullOrEmpty()) + { + continue; + } + + var fileName = item.FileName; + if (!File.Exists(fileName)) + { + continue; + } + var toPath = Utils.GetBinPath("", item.CoreType); + + if (fileName.Contains(".tar.gz")) + { + FileManager.DecompressTarFile(fileName, toPath); + var dir = new DirectoryInfo(toPath); + if (dir.Exists) + { + foreach (var subDir in dir.GetDirectories()) + { + FileManager.CopyDirectory(subDir.FullName, toPath, false, true); + subDir.Delete(true); + } + } + } + else if (fileName.Contains(".gz")) + { + FileManager.DecompressFile(fileName, toPath, item.CoreType); + } + else + { + FileManager.ZipExtractToFile(fileName, toPath, "geo"); + } + + if (Utils.IsNonWindows()) + { + var filesList = (new DirectoryInfo(toPath)).GetFiles().Select(u => u.FullName).ToList(); + foreach (var file in filesList) + { + await Utils.SetLinuxChmod(Path.Combine(toPath, item.CoreType.ToLower())); + } + } + + await UpdateView(item.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfully); + + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + } + } + + private async Task UpdateView(string coreType, string msg) + { + var item = new CheckUpdateModel() + { + CoreType = coreType, + Remarks = msg, + }; + + RxApp.MainThreadScheduler.Schedule(item, (scheduler, model) => + { + _ = UpdateViewResult(model); + return Disposable.Empty; + }); + } + + public async Task UpdateViewResult(CheckUpdateModel model) + { + var found = CheckUpdateModels.FirstOrDefault(t => t.CoreType == model.CoreType); + if (found == null) + { + return; + } + + var itemCopy = JsonUtils.DeepCopy(found); + itemCopy.Remarks = model.Remarks; + CheckUpdateModels.Replace(found, itemCopy); + } +} diff --git a/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs new file mode 100644 index 00000000..d45b8e7d --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs @@ -0,0 +1,158 @@ +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class ClashConnectionsViewModel : MyReactiveObject +{ + public IObservableCollection ConnectionItems { get; } = new ObservableCollectionExtended(); + + [Reactive] + public ClashConnectionModel SelectedSource { get; set; } + + public ReactiveCommand ConnectionCloseCmd { get; } + public ReactiveCommand ConnectionCloseAllCmd { get; } + + [Reactive] + public string HostFilter { get; set; } + + [Reactive] + public bool AutoRefresh { get; set; } + + public ClashConnectionsViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh; + + var canEditRemove = this.WhenAnyValue( + x => x.SelectedSource, + selectedSource => selectedSource != null && selectedSource.Id.IsNotEmpty()); + + this.WhenAnyValue( + x => x.AutoRefresh, + y => y == true) + .Subscribe(c => { _config.ClashUIItem.ConnectionsAutoRefresh = AutoRefresh; }); + ConnectionCloseCmd = ReactiveCommand.CreateFromTask(async () => + { + await ClashConnectionClose(false); + }, canEditRemove); + + ConnectionCloseAllCmd = ReactiveCommand.CreateFromTask(async () => + { + await ClashConnectionClose(true); + }); + + _ = Init(); + } + + private async Task Init() + { + await DelayTestTask(); + } + + private async Task GetClashConnections() + { + var ret = await ClashApiManager.Instance.GetClashConnectionsAsync(); + if (ret == null) + { + return; + } + + RxApp.MainThreadScheduler.Schedule(ret?.connections, (scheduler, model) => + { + _ = RefreshConnections(model); + return Disposable.Empty; + }); + } + + public async Task RefreshConnections(List? connections) + { + ConnectionItems.Clear(); + + var dtNow = DateTime.Now; + var lstModel = new List(); + foreach (var item in connections ?? new()) + { + var host = $"{(item.metadata.host.IsNullOrEmpty() ? item.metadata.destinationIP : item.metadata.host)}:{item.metadata.destinationPort}"; + if (HostFilter.IsNotEmpty() && !host.Contains(HostFilter)) + { + continue; + } + + var model = new ClashConnectionModel + { + Id = item.id, + Network = item.metadata.network, + Type = item.metadata.type, + Host = host, + Time = (dtNow - item.start).TotalSeconds < 0 ? 1 : (dtNow - item.start).TotalSeconds, + Elapsed = (dtNow - item.start).ToString(@"hh\:mm\:ss"), + Chain = $"{item.rule} , {string.Join("->", item.chains ?? new())}" + }; + + lstModel.Add(model); + } + if (lstModel.Count <= 0) + { + return; + } + + ConnectionItems.AddRange(lstModel); + } + + public async Task ClashConnectionClose(bool all) + { + var id = string.Empty; + if (!all) + { + var item = SelectedSource; + if (item is null) + { + return; + } + id = item.Id; + } + else + { + ConnectionItems.Clear(); + } + await ClashApiManager.Instance.ClashConnectionClose(id); + await GetClashConnections(); + } + + public async Task DelayTestTask() + { + _ = Task.Run(async () => + { + var numOfExecuted = 1; + while (true) + { + await Task.Delay(1000 * 5); + numOfExecuted++; + if (!(AutoRefresh && _config.UiItem.ShowInTaskbar && _config.IsRunningCore(ECoreType.sing_box))) + { + continue; + } + + if (_config.ClashUIItem.ConnectionsRefreshInterval <= 0) + { + continue; + } + + if (numOfExecuted % _config.ClashUIItem.ConnectionsRefreshInterval != 0) + { + continue; + } + await GetClashConnections(); + } + }); + + await Task.CompletedTask; + } +} diff --git a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs new file mode 100644 index 00000000..54cbdc8d --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs @@ -0,0 +1,452 @@ +using System.Reactive; +using System.Reactive.Concurrency; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using static ServiceLib.Models.ClashProviders; +using static ServiceLib.Models.ClashProxies; + +namespace ServiceLib.ViewModels; + +public class ClashProxiesViewModel : MyReactiveObject +{ + private Dictionary? _proxies; + private Dictionary? _providers; + private readonly int _delayTimeout = 99999999; + + public IObservableCollection ProxyGroups { get; } = new ObservableCollectionExtended(); + public IObservableCollection ProxyDetails { get; } = new ObservableCollectionExtended(); + + [Reactive] + public ClashProxyModel SelectedGroup { get; set; } + + [Reactive] + public ClashProxyModel SelectedDetail { get; set; } + + public ReactiveCommand ProxiesReloadCmd { get; } + public ReactiveCommand ProxiesDelayTestCmd { get; } + public ReactiveCommand ProxiesDelayTestPartCmd { get; } + public ReactiveCommand ProxiesSelectActivityCmd { get; } + + [Reactive] + public int RuleModeSelected { get; set; } + + [Reactive] + public int SortingSelected { get; set; } + + [Reactive] + public bool AutoRefresh { get; set; } + + public ClashProxiesViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + ProxiesReloadCmd = ReactiveCommand.CreateFromTask(async () => + { + await ProxiesReload(); + }); + ProxiesDelayTestCmd = ReactiveCommand.CreateFromTask(async () => + { + await ProxiesDelayTest(true); + }); + + ProxiesDelayTestPartCmd = ReactiveCommand.CreateFromTask(async () => + { + await ProxiesDelayTest(false); + }); + ProxiesSelectActivityCmd = ReactiveCommand.CreateFromTask(async () => + { + await SetActiveProxy(); + }); + + SelectedGroup = new(); + SelectedDetail = new(); + AutoRefresh = _config.ClashUIItem.ProxiesAutoRefresh; + SortingSelected = _config.ClashUIItem.ProxiesSorting; + RuleModeSelected = (int)_config.ClashUIItem.RuleMode; + + this.WhenAnyValue( + x => x.SelectedGroup, + y => y != null && y.Name.IsNotEmpty()) + .Subscribe(c => RefreshProxyDetails(c)); + + this.WhenAnyValue( + x => x.RuleModeSelected, + y => y >= 0) + .Subscribe(async c => await DoRuleModeSelected(c)); + + this.WhenAnyValue( + x => x.SortingSelected, + y => y >= 0) + .Subscribe(c => DoSortingSelected(c)); + + this.WhenAnyValue( + x => x.AutoRefresh, + y => y == true) + .Subscribe(c => { _config.ClashUIItem.ProxiesAutoRefresh = AutoRefresh; }); + + _ = Init(); + } + + private async Task Init() + { + await DelayTestTask(); + } + + private async Task DoRuleModeSelected(bool c) + { + if (!c) + { + return; + } + if (_config.ClashUIItem.RuleMode == (ERuleMode)RuleModeSelected) + { + return; + } + await SetRuleModeCheck((ERuleMode)RuleModeSelected); + } + + public async Task SetRuleModeCheck(ERuleMode mode) + { + if (_config.ClashUIItem.RuleMode == mode) + { + return; + } + await SetRuleMode(mode); + } + + private void DoSortingSelected(bool c) + { + if (!c) + { + return; + } + if (SortingSelected != _config.ClashUIItem.ProxiesSorting) + { + _config.ClashUIItem.ProxiesSorting = SortingSelected; + } + + RefreshProxyDetails(c); + } + + public async Task ProxiesReload() + { + await GetClashProxies(true); + await ProxiesDelayTest(); + } + + #region proxy function + + private async Task SetRuleMode(ERuleMode mode) + { + _config.ClashUIItem.RuleMode = mode; + + if (mode != ERuleMode.Unchanged) + { + Dictionary headers = new() + { + { "mode", mode.ToString().ToLower() } + }; + await ClashApiManager.Instance.ClashConfigUpdate(headers); + } + } + + private async Task GetClashProxies(bool refreshUI) + { + var ret = await ClashApiManager.Instance.GetClashProxiesAsync(); + if (ret?.Item1 == null || ret.Item2 == null) + { + return; + } + _proxies = ret.Item1.proxies; + _providers = ret?.Item2.providers; + + if (refreshUI) + { + RxApp.MainThreadScheduler.Schedule(() => _ = RefreshProxyGroups()); + } + } + + public async Task RefreshProxyGroups() + { + if (_proxies == null) + { + return; + } + + var selectedName = SelectedGroup?.Name; + ProxyGroups.Clear(); + + var proxyGroups = ClashApiManager.Instance.GetClashProxyGroups(); + if (proxyGroups != null && proxyGroups.Count > 0) + { + foreach (var it in proxyGroups) + { + if (it.name.IsNullOrEmpty() || !_proxies.ContainsKey(it.name)) + { + continue; + } + var item = _proxies[it.name]; + if (!Global.allowSelectType.Contains(item.type.ToLower())) + { + continue; + } + ProxyGroups.Add(new ClashProxyModel() + { + Now = item.now, + Name = item.name, + Type = item.type + }); + } + } + + //from api + foreach (KeyValuePair kv in _proxies) + { + if (!Global.allowSelectType.Contains(kv.Value.type.ToLower())) + { + continue; + } + var item = ProxyGroups.FirstOrDefault(t => t.Name == kv.Key); + if (item != null && item.Name.IsNotEmpty()) + { + continue; + } + ProxyGroups.Add(new ClashProxyModel() + { + Now = kv.Value.now, + Name = kv.Key, + Type = kv.Value.type + }); + } + + if (ProxyGroups != null && ProxyGroups.Count > 0) + { + if (selectedName != null && ProxyGroups.Any(t => t.Name == selectedName)) + { + SelectedGroup = ProxyGroups.FirstOrDefault(t => t.Name == selectedName); + } + else + { + SelectedGroup = ProxyGroups.First(); + } + } + else + { + SelectedGroup = new(); + } + } + + private void RefreshProxyDetails(bool c) + { + ProxyDetails.Clear(); + if (!c) + { + return; + } + var name = SelectedGroup?.Name; + if (name.IsNullOrEmpty()) + { + return; + } + if (_proxies == null) + { + return; + } + + _proxies.TryGetValue(name, out var proxy); + if (proxy?.all == null) + { + return; + } + var lstDetails = new List(); + foreach (var item in proxy.all) + { + var proxy2 = TryGetProxy(item); + if (proxy2 == null) + { + continue; + } + var delay = proxy2.history?.Count > 0 ? proxy2.history.Last().delay : -1; + + lstDetails.Add(new ClashProxyModel() + { + IsActive = item == proxy.now, + Name = item, + Type = proxy2.type, + Delay = delay <= 0 ? _delayTimeout : delay, + DelayName = delay <= 0 ? string.Empty : $"{delay}ms", + }); + } + //sort + switch (SortingSelected) + { + case 0: + lstDetails = lstDetails.OrderBy(t => t.Delay).ToList(); + break; + + case 1: + lstDetails = lstDetails.OrderBy(t => t.Name).ToList(); + break; + + default: + break; + } + ProxyDetails.AddRange(lstDetails); + } + + private ProxiesItem? TryGetProxy(string name) + { + if (_proxies == null) + { + return null; + } + _proxies.TryGetValue(name, out var proxy2); + if (proxy2 != null) + { + return proxy2; + } + //from providers + if (_providers != null) + { + foreach (KeyValuePair kv in _providers) + { + if (Global.proxyVehicleType.Contains(kv.Value.vehicleType.ToLower())) + { + var proxy3 = kv.Value.proxies.FirstOrDefault(t => t.name == name); + if (proxy3 != null) + { + return proxy3; + } + } + } + } + return null; + } + + public async Task SetActiveProxy() + { + if (SelectedGroup == null || SelectedGroup.Name.IsNullOrEmpty()) + { + return; + } + if (SelectedDetail == null || SelectedDetail.Name.IsNullOrEmpty()) + { + return; + } + var name = SelectedGroup.Name; + if (name.IsNullOrEmpty()) + { + return; + } + var nameNode = SelectedDetail.Name; + if (nameNode.IsNullOrEmpty()) + { + return; + } + var selectedProxy = TryGetProxy(name); + if (selectedProxy == null || selectedProxy.type != "Selector") + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + return; + } + + await ClashApiManager.Instance.ClashSetActiveProxy(name, nameNode); + + selectedProxy.now = nameNode; + var group = ProxyGroups.FirstOrDefault(it => it.Name == SelectedGroup.Name); + if (group != null) + { + group.Now = nameNode; + var group2 = JsonUtils.DeepCopy(group); + ProxyGroups.Replace(group, group2); + + SelectedGroup = group2; + } + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + } + + private async Task ProxiesDelayTest(bool blAll = true) + { + ClashApiManager.Instance.ClashProxiesDelayTest(blAll, ProxyDetails.ToList(), async (item, result) => + { + if (item == null || result.IsNullOrEmpty()) + { + return; + } + + var model = new SpeedTestResult() { IndexId = item.Name, Delay = result }; + RxApp.MainThreadScheduler.Schedule(model, (scheduler, model) => + { + _ = ProxiesDelayTestResult(model); + return Disposable.Empty; + }); + }); + await Task.CompletedTask; + } + + public async Task ProxiesDelayTestResult(SpeedTestResult result) + { + //UpdateHandler(false, $"{item.name}={result}"); + var detail = ProxyDetails.FirstOrDefault(it => it.Name == result.IndexId); + if (detail == null) + { + return; + } + + var dicResult = JsonUtils.Deserialize>(result.Delay); + if (dicResult != null && dicResult.TryGetValue("delay", out var value)) + { + detail.Delay = Convert.ToInt32(value.ToString()); + detail.DelayName = $"{detail.Delay}ms"; + } + else if (dicResult != null && dicResult.TryGetValue("message", out var value1)) + { + detail.Delay = _delayTimeout; + detail.DelayName = $"{value1}"; + } + else + { + detail.Delay = _delayTimeout; + detail.DelayName = string.Empty; + } + ProxyDetails.Replace(detail, JsonUtils.DeepCopy(detail)); + } + + #endregion proxy function + + #region task + + public async Task DelayTestTask() + { + _ = Task.Run(async () => + { + var numOfExecuted = 1; + while (true) + { + await Task.Delay(1000 * 60); + numOfExecuted++; + if (!(AutoRefresh && _config.UiItem.ShowInTaskbar && _config.IsRunningCore(ECoreType.sing_box))) + { + continue; + } + if (_config.ClashUIItem.ProxiesAutoDelayTestInterval <= 0) + { + continue; + } + if (numOfExecuted % _config.ClashUIItem.ProxiesAutoDelayTestInterval != 0) + { + continue; + } + await ProxiesDelayTest(); + } + }); + await Task.CompletedTask; + } + + #endregion task +} diff --git a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs new file mode 100644 index 00000000..9ca2d407 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs @@ -0,0 +1,166 @@ +using System.Reactive; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class DNSSettingViewModel : MyReactiveObject +{ + [Reactive] public bool? UseSystemHosts { get; set; } + [Reactive] public bool? AddCommonHosts { get; set; } + [Reactive] public bool? FakeIP { get; set; } + [Reactive] public bool? BlockBindingQuery { get; set; } + [Reactive] public string? DirectDNS { get; set; } + [Reactive] public string? RemoteDNS { get; set; } + [Reactive] public string? SingboxOutboundsResolveDNS { get; set; } + [Reactive] public string? SingboxFinalResolveDNS { get; set; } + [Reactive] public string? RayStrategy4Freedom { get; set; } + [Reactive] public string? SingboxStrategy4Direct { get; set; } + [Reactive] public string? SingboxStrategy4Proxy { get; set; } + [Reactive] public string? Hosts { get; set; } + [Reactive] public string? DirectExpectedIPs { get; set; } + + [Reactive] public bool UseSystemHostsCompatible { get; set; } + [Reactive] public string DomainStrategy4FreedomCompatible { get; set; } + [Reactive] public string DomainDNSAddressCompatible { get; set; } + [Reactive] public string NormalDNSCompatible { get; set; } + + [Reactive] public string DomainStrategy4Freedom2Compatible { get; set; } + [Reactive] public string DomainDNSAddress2Compatible { get; set; } + [Reactive] public string NormalDNS2Compatible { get; set; } + [Reactive] public string TunDNS2Compatible { get; set; } + [Reactive] public bool RayCustomDNSEnableCompatible { get; set; } + [Reactive] public bool SBCustomDNSEnableCompatible { get; set; } + + public ReactiveCommand SaveCmd { get; } + public ReactiveCommand ImportDefConfig4V2rayCompatibleCmd { get; } + public ReactiveCommand ImportDefConfig4SingboxCompatibleCmd { get; } + + public DNSSettingViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + SaveCmd = ReactiveCommand.CreateFromTask(SaveSettingAsync); + + ImportDefConfig4V2rayCompatibleCmd = ReactiveCommand.CreateFromTask(async () => + { + NormalDNSCompatible = EmbedUtils.GetEmbedText(Global.DNSV2rayNormalFileName); + await Task.CompletedTask; + }); + + ImportDefConfig4SingboxCompatibleCmd = ReactiveCommand.CreateFromTask(async () => + { + NormalDNS2Compatible = EmbedUtils.GetEmbedText(Global.DNSSingboxNormalFileName); + TunDNS2Compatible = EmbedUtils.GetEmbedText(Global.TunSingboxDNSFileName); + await Task.CompletedTask; + }); + + _ = Init(); + } + + private async Task Init() + { + _config = AppManager.Instance.Config; + var item = _config.SimpleDNSItem; + UseSystemHosts = item.UseSystemHosts; + AddCommonHosts = item.AddCommonHosts; + FakeIP = item.FakeIP; + BlockBindingQuery = item.BlockBindingQuery; + DirectDNS = item.DirectDNS; + RemoteDNS = item.RemoteDNS; + RayStrategy4Freedom = item.RayStrategy4Freedom; + SingboxOutboundsResolveDNS = item.SingboxOutboundsResolveDNS; + SingboxFinalResolveDNS = item.SingboxFinalResolveDNS; + SingboxStrategy4Direct = item.SingboxStrategy4Direct; + SingboxStrategy4Proxy = item.SingboxStrategy4Proxy; + Hosts = item.Hosts; + DirectExpectedIPs = item.DirectExpectedIPs; + + var item1 = await AppManager.Instance.GetDNSItem(ECoreType.Xray); + RayCustomDNSEnableCompatible = item1.Enabled; + UseSystemHostsCompatible = item1.UseSystemHosts; + DomainStrategy4FreedomCompatible = item1?.DomainStrategy4Freedom ?? string.Empty; + DomainDNSAddressCompatible = item1?.DomainDNSAddress ?? string.Empty; + NormalDNSCompatible = item1?.NormalDNS ?? string.Empty; + + var item2 = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); + SBCustomDNSEnableCompatible = item2.Enabled; + DomainStrategy4Freedom2Compatible = item2?.DomainStrategy4Freedom ?? string.Empty; + DomainDNSAddress2Compatible = item2?.DomainDNSAddress ?? string.Empty; + NormalDNS2Compatible = item2?.NormalDNS ?? string.Empty; + TunDNS2Compatible = item2?.TunDNS ?? string.Empty; + } + + private async Task SaveSettingAsync() + { + _config.SimpleDNSItem.UseSystemHosts = UseSystemHosts; + _config.SimpleDNSItem.AddCommonHosts = AddCommonHosts; + _config.SimpleDNSItem.FakeIP = FakeIP; + _config.SimpleDNSItem.BlockBindingQuery = BlockBindingQuery; + _config.SimpleDNSItem.DirectDNS = DirectDNS; + _config.SimpleDNSItem.RemoteDNS = RemoteDNS; + _config.SimpleDNSItem.RayStrategy4Freedom = RayStrategy4Freedom; + _config.SimpleDNSItem.SingboxOutboundsResolveDNS = SingboxOutboundsResolveDNS; + _config.SimpleDNSItem.SingboxFinalResolveDNS = SingboxFinalResolveDNS; + _config.SimpleDNSItem.SingboxStrategy4Direct = SingboxStrategy4Direct; + _config.SimpleDNSItem.SingboxStrategy4Proxy = SingboxStrategy4Proxy; + _config.SimpleDNSItem.Hosts = Hosts; + _config.SimpleDNSItem.DirectExpectedIPs = DirectExpectedIPs; + + if (NormalDNSCompatible.IsNotEmpty()) + { + var obj = JsonUtils.ParseJson(NormalDNSCompatible); + if (obj != null && obj["servers"] != null) + { + } + else + { + if (NormalDNSCompatible.Contains('{') || NormalDNSCompatible.Contains('}')) + { + NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText); + return; + } + } + } + if (NormalDNS2Compatible.IsNotEmpty()) + { + var obj2 = JsonUtils.Deserialize(NormalDNS2Compatible); + if (obj2 == null) + { + NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText); + return; + } + } + if (TunDNS2Compatible.IsNotEmpty()) + { + var obj2 = JsonUtils.Deserialize(TunDNS2Compatible); + if (obj2 == null) + { + NoticeManager.Instance.Enqueue(ResUI.FillCorrectDNSText); + return; + } + } + + var item1 = await AppManager.Instance.GetDNSItem(ECoreType.Xray); + item1.Enabled = RayCustomDNSEnableCompatible; + item1.DomainStrategy4Freedom = DomainStrategy4FreedomCompatible; + item1.DomainDNSAddress = DomainDNSAddressCompatible; + item1.UseSystemHosts = UseSystemHostsCompatible; + item1.NormalDNS = NormalDNSCompatible; + await ConfigHandler.SaveDNSItems(_config, item1); + + var item2 = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); + item2.Enabled = SBCustomDNSEnableCompatible; + item2.DomainStrategy4Freedom = DomainStrategy4Freedom2Compatible; + item2.DomainDNSAddress = DomainDNSAddress2Compatible; + item2.NormalDNS = JsonUtils.Serialize(JsonUtils.ParseJson(NormalDNS2Compatible)); + item2.TunDNS = JsonUtils.Serialize(JsonUtils.ParseJson(TunDNS2Compatible)); + await ConfigHandler.SaveDNSItems(_config, item2); + + await ConfigHandler.SaveConfig(_config); + if (_updateView != null) + { + await _updateView(EViewAction.CloseWindow, null); + } + } +} diff --git a/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs b/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs new file mode 100644 index 00000000..3619ddef --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs @@ -0,0 +1,113 @@ +using System.Reactive; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class FullConfigTemplateViewModel : MyReactiveObject +{ + #region Reactive + + [Reactive] + public bool EnableFullConfigTemplate4Ray { get; set; } + + [Reactive] + public bool EnableFullConfigTemplate4Singbox { get; set; } + + [Reactive] + public string FullConfigTemplate4Ray { get; set; } + + [Reactive] + public string FullConfigTemplate4Singbox { get; set; } + + [Reactive] + public string FullTunConfigTemplate4Singbox { get; set; } + + [Reactive] + public bool AddProxyOnly4Ray { get; set; } + + [Reactive] + public bool AddProxyOnly4Singbox { get; set; } + + [Reactive] + public string ProxyDetour4Ray { get; set; } + + [Reactive] + public string ProxyDetour4Singbox { get; set; } + + public ReactiveCommand SaveCmd { get; } + + #endregion Reactive + + public FullConfigTemplateViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + SaveCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveSettingAsync(); + }); + + _ = Init(); + } + + private async Task Init() + { + var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray); + EnableFullConfigTemplate4Ray = item?.Enabled ?? false; + FullConfigTemplate4Ray = item?.Config ?? string.Empty; + AddProxyOnly4Ray = item?.AddProxyOnly ?? false; + ProxyDetour4Ray = item?.ProxyDetour ?? string.Empty; + + var item2 = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box); + EnableFullConfigTemplate4Singbox = item2?.Enabled ?? false; + FullConfigTemplate4Singbox = item2?.Config ?? string.Empty; + FullTunConfigTemplate4Singbox = item2?.TunConfig ?? string.Empty; + AddProxyOnly4Singbox = item2?.AddProxyOnly ?? false; + ProxyDetour4Singbox = item2?.ProxyDetour ?? string.Empty; + } + + private async Task SaveSettingAsync() + { + if (!await SaveXrayConfigAsync()) + return; + + if (!await SaveSingboxConfigAsync()) + return; + + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + _ = _updateView?.Invoke(EViewAction.CloseWindow, null); + } + + private async Task SaveXrayConfigAsync() + { + var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray); + item.Enabled = EnableFullConfigTemplate4Ray; + item.Config = null; + + item.Config = FullConfigTemplate4Ray; + + item.AddProxyOnly = AddProxyOnly4Ray; + item.ProxyDetour = ProxyDetour4Ray; + + await ConfigHandler.SaveFullConfigTemplate(_config, item); + return true; + } + + private async Task SaveSingboxConfigAsync() + { + var item = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.sing_box); + item.Enabled = EnableFullConfigTemplate4Singbox; + item.Config = null; + item.TunConfig = null; + + item.Config = FullConfigTemplate4Singbox; + item.TunConfig = FullTunConfigTemplate4Singbox; + + item.AddProxyOnly = AddProxyOnly4Singbox; + item.ProxyDetour = ProxyDetour4Singbox; + + await ConfigHandler.SaveFullConfigTemplate(_config, item); + return true; + } +} diff --git a/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs new file mode 100644 index 00000000..0acb8726 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs @@ -0,0 +1,64 @@ +using System.Reactive; +using ReactiveUI; + +namespace ServiceLib.ViewModels; + +public class GlobalHotkeySettingViewModel : MyReactiveObject +{ + private readonly List _globalHotkeys; + + public ReactiveCommand SaveCmd { get; } + + public GlobalHotkeySettingViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + _globalHotkeys = JsonUtils.DeepCopy(_config.GlobalHotkeys); + + SaveCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveSettingAsync(); + }); + } + + public KeyEventItem GetKeyEventItem(EGlobalHotkey eg) + { + var item = _globalHotkeys.FirstOrDefault((it) => it.EGlobalHotkey == eg); + if (item != null) + { + return item; + } + + item = new() + { + EGlobalHotkey = eg, + Control = false, + Alt = false, + Shift = false, + KeyCode = null + }; + _globalHotkeys.Add(item); + + return item; + } + + public void ResetKeyEventItem() + { + _globalHotkeys.Clear(); + } + + private async Task SaveSettingAsync() + { + _config.GlobalHotkeys = _globalHotkeys; + + if (await ConfigHandler.SaveConfig(_config) == 0) + { + _updateView?.Invoke(EViewAction.CloseWindow, null); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } +} diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs new file mode 100644 index 00000000..4e9fc9fc --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,585 @@ +using System.Reactive; +using System.Reactive.Concurrency; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Splat; + +namespace ServiceLib.ViewModels; + +public class MainWindowViewModel : MyReactiveObject +{ + #region Menu + + //servers + public ReactiveCommand AddVmessServerCmd { get; } + + public ReactiveCommand AddVlessServerCmd { get; } + public ReactiveCommand AddShadowsocksServerCmd { get; } + public ReactiveCommand AddSocksServerCmd { get; } + public ReactiveCommand AddHttpServerCmd { get; } + public ReactiveCommand AddTrojanServerCmd { get; } + public ReactiveCommand AddHysteria2ServerCmd { get; } + public ReactiveCommand AddTuicServerCmd { get; } + public ReactiveCommand AddWireguardServerCmd { get; } + public ReactiveCommand AddAnytlsServerCmd { get; } + public ReactiveCommand AddCustomServerCmd { get; } + public ReactiveCommand AddServerViaClipboardCmd { get; } + public ReactiveCommand AddServerViaScanCmd { get; } + public ReactiveCommand AddServerViaImageCmd { get; } + + //Subscription + public ReactiveCommand SubSettingCmd { get; } + + public ReactiveCommand SubUpdateCmd { get; } + public ReactiveCommand SubUpdateViaProxyCmd { get; } + public ReactiveCommand SubGroupUpdateCmd { get; } + public ReactiveCommand SubGroupUpdateViaProxyCmd { get; } + + //Setting + public ReactiveCommand OptionSettingCmd { get; } + + public ReactiveCommand RoutingSettingCmd { get; } + public ReactiveCommand DNSSettingCmd { get; } + public ReactiveCommand FullConfigTemplateCmd { get; } + public ReactiveCommand GlobalHotkeySettingCmd { get; } + public ReactiveCommand RebootAsAdminCmd { get; } + public ReactiveCommand ClearServerStatisticsCmd { get; } + public ReactiveCommand OpenTheFileLocationCmd { get; } + + //Presets + public ReactiveCommand RegionalPresetDefaultCmd { get; } + + public ReactiveCommand RegionalPresetRussiaCmd { get; } + + public ReactiveCommand RegionalPresetIranCmd { get; } + + public ReactiveCommand ReloadCmd { get; } + + [Reactive] + public bool BlReloadEnabled { get; set; } + + [Reactive] + public bool ShowClashUI { get; set; } + + [Reactive] + public int TabMainSelectedIndex { get; set; } + + #endregion Menu + + private bool _hasNextReloadJob = false; + + #region Init + + public MainWindowViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + #region WhenAnyValue && ReactiveCommand + + //servers + AddVmessServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.VMess); + }); + AddVlessServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.VLESS); + }); + AddShadowsocksServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.Shadowsocks); + }); + AddSocksServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.SOCKS); + }); + AddHttpServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.HTTP); + }); + AddTrojanServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.Trojan); + }); + AddHysteria2ServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.Hysteria2); + }); + AddTuicServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.TUIC); + }); + AddWireguardServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.WireGuard); + }); + AddAnytlsServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.Anytls); + }); + AddCustomServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.Custom); + }); + AddServerViaClipboardCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerViaClipboardAsync(null); + }); + AddServerViaScanCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerViaScanAsync(); + }); + AddServerViaImageCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerViaImageAsync(); + }); + + //Subscription + SubSettingCmd = ReactiveCommand.CreateFromTask(async () => + { + await SubSettingAsync(); + }); + + SubUpdateCmd = ReactiveCommand.CreateFromTask(async () => + { + await UpdateSubscriptionProcess("", false); + }); + SubUpdateViaProxyCmd = ReactiveCommand.CreateFromTask(async () => + { + await UpdateSubscriptionProcess("", true); + }); + SubGroupUpdateCmd = ReactiveCommand.CreateFromTask(async () => + { + await UpdateSubscriptionProcess(_config.SubIndexId, false); + }); + SubGroupUpdateViaProxyCmd = ReactiveCommand.CreateFromTask(async () => + { + await UpdateSubscriptionProcess(_config.SubIndexId, true); + }); + + //Setting + OptionSettingCmd = ReactiveCommand.CreateFromTask(async () => + { + await OptionSettingAsync(); + }); + RoutingSettingCmd = ReactiveCommand.CreateFromTask(async () => + { + await RoutingSettingAsync(); + }); + DNSSettingCmd = ReactiveCommand.CreateFromTask(async () => + { + await DNSSettingAsync(); + }); + FullConfigTemplateCmd = ReactiveCommand.CreateFromTask(async () => + { + await FullConfigTemplateAsync(); + }); + GlobalHotkeySettingCmd = ReactiveCommand.CreateFromTask(async () => + { + if (await _updateView?.Invoke(EViewAction.GlobalHotkeySettingWindow, null) == true) + { + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + } + }); + RebootAsAdminCmd = ReactiveCommand.CreateFromTask(async () => + { + await RebootAsAdmin(); + }); + ClearServerStatisticsCmd = ReactiveCommand.CreateFromTask(async () => + { + await ClearServerStatistics(); + }); + OpenTheFileLocationCmd = ReactiveCommand.CreateFromTask(async () => + { + await OpenTheFileLocation(); + }); + + ReloadCmd = ReactiveCommand.CreateFromTask(async () => + { + await Reload(); + }); + + RegionalPresetDefaultCmd = ReactiveCommand.CreateFromTask(async () => + { + await ApplyRegionalPreset(EPresetType.Default); + }); + + RegionalPresetRussiaCmd = ReactiveCommand.CreateFromTask(async () => + { + await ApplyRegionalPreset(EPresetType.Russia); + }); + + RegionalPresetIranCmd = ReactiveCommand.CreateFromTask(async () => + { + await ApplyRegionalPreset(EPresetType.Iran); + }); + + #endregion WhenAnyValue && ReactiveCommand + + _ = Init(); + } + + private async Task Init() + { + _config.UiItem.ShowInTaskbar = true; + + await ConfigHandler.InitBuiltinRouting(_config); + await ConfigHandler.InitBuiltinDNS(_config); + await ConfigHandler.InitBuiltinFullConfigTemplate(_config); + await ProfileExManager.Instance.Init(); + await CoreManager.Instance.Init(_config, UpdateHandler); + TaskManager.Instance.RegUpdateTask(_config, UpdateTaskHandler); + + if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed) + { + await StatisticsManager.Instance.Init(_config, UpdateStatisticsHandler); + } + await RefreshServers(); + + BlReloadEnabled = true; + await Reload(); + await AutoHideStartup(); + Locator.Current.GetService()?.RefreshRoutingsMenu(); + } + + #endregion Init + + #region Actions + + private async Task UpdateHandler(bool notify, string msg) + { + NoticeManager.Instance.SendMessage(msg); + if (notify) + { + NoticeManager.Instance.Enqueue(msg); + } + } + + private async Task UpdateTaskHandler(bool success, string msg) + { + NoticeManager.Instance.SendMessageEx(msg); + if (success) + { + var indexIdOld = _config.IndexId; + await RefreshServers(); + if (indexIdOld != _config.IndexId) + { + await Reload(); + } + if (_config.UiItem.EnableAutoAdjustMainLvColWidth) + { + AppEvents.AdjustMainLvColWidthRequested.OnNext(Unit.Default); + } + } + } + + private async Task UpdateStatisticsHandler(ServerSpeedItem update) + { + if (!_config.UiItem.ShowInTaskbar) + { + return; + } + AppEvents.DispatcherStatisticsRequested.OnNext(update); + } + + public void ShowHideWindow(bool? blShow) + { + _updateView?.Invoke(EViewAction.ShowHideWindow, blShow); + } + + #endregion Actions + + #region Servers && Groups + + private async Task RefreshServers() + { + AppEvents.ProfilesRefreshRequested.OnNext(Unit.Default); + + await Task.Delay(200); + } + + private void RefreshSubscriptions() + { + Locator.Current.GetService()?.RefreshSubscriptions(); + } + + #endregion Servers && Groups + + #region Add Servers + + public async Task AddServerAsync(bool blNew, EConfigType eConfigType) + { + ProfileItem item = new() + { + Subid = _config.SubIndexId, + ConfigType = eConfigType, + IsSub = false, + }; + + bool? ret = false; + if (eConfigType == EConfigType.Custom) + { + ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); + } + else + { + ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); + } + if (ret == true) + { + await RefreshServers(); + if (item.IndexId == _config.IndexId) + { + await Reload(); + } + } + } + + public async Task AddServerViaClipboardAsync(string? clipboardData) + { + if (clipboardData == null) + { + await _updateView?.Invoke(EViewAction.AddServerViaClipboard, null); + return; + } + var ret = await ConfigHandler.AddBatchServers(_config, clipboardData, _config.SubIndexId, false); + if (ret > 0) + { + RefreshSubscriptions(); + await RefreshServers(); + NoticeManager.Instance.Enqueue(string.Format(ResUI.SuccessfullyImportedServerViaClipboard, ret)); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } + + public async Task AddServerViaScanAsync() + { + _updateView?.Invoke(EViewAction.ScanScreenTask, null); + await Task.CompletedTask; + } + + public async Task ScanScreenResult(byte[]? bytes) + { + var result = QRCodeUtils.ParseBarcode(bytes); + await AddScanResultAsync(result); + } + + public async Task AddServerViaImageAsync() + { + _updateView?.Invoke(EViewAction.ScanImageTask, null); + await Task.CompletedTask; + } + + public async Task ScanImageResult(string fileName) + { + if (fileName.IsNullOrEmpty()) + { + return; + } + + var result = QRCodeUtils.ParseBarcode(fileName); + await AddScanResultAsync(result); + } + + private async Task AddScanResultAsync(string? result) + { + if (result.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.NoValidQRcodeFound); + } + else + { + var ret = await ConfigHandler.AddBatchServers(_config, result, _config.SubIndexId, false); + if (ret > 0) + { + RefreshSubscriptions(); + await RefreshServers(); + NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedServerViaScan); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } + } + + #endregion Add Servers + + #region Subscription + + private async Task SubSettingAsync() + { + if (await _updateView?.Invoke(EViewAction.SubSettingWindow, null) == true) + { + RefreshSubscriptions(); + } + } + + public async Task UpdateSubscriptionProcess(string subId, bool blProxy) + { + await Task.Run(async () => await SubscriptionHandler.UpdateProcess(_config, subId, blProxy, UpdateTaskHandler)); + } + + #endregion Subscription + + #region Setting + + private async Task OptionSettingAsync() + { + var ret = await _updateView?.Invoke(EViewAction.OptionSettingWindow, null); + if (ret == true) + { + Locator.Current.GetService()?.InboundDisplayStatus(); + await Reload(); + } + } + + private async Task RoutingSettingAsync() + { + var ret = await _updateView?.Invoke(EViewAction.RoutingSettingWindow, null); + if (ret == true) + { + await ConfigHandler.InitBuiltinRouting(_config); + Locator.Current.GetService()?.RefreshRoutingsMenu(); + await Reload(); + } + } + + private async Task DNSSettingAsync() + { + var ret = await _updateView?.Invoke(EViewAction.DNSSettingWindow, null); + if (ret == true) + { + await Reload(); + } + } + + private async Task FullConfigTemplateAsync() + { + var ret = await _updateView?.Invoke(EViewAction.FullConfigTemplateWindow, null); + if (ret == true) + { + await Reload(); + } + } + + public async Task RebootAsAdmin() + { + ProcUtils.RebootAsAdmin(); + await AppManager.Instance.AppExitAsync(true); + } + + private async Task ClearServerStatistics() + { + await StatisticsManager.Instance.ClearAllServerStatistics(); + await RefreshServers(); + } + + private async Task OpenTheFileLocation() + { + var path = Utils.StartupPath(); + if (Utils.IsWindows()) + { + ProcUtils.ProcessStart(path); + } + else if (Utils.IsLinux()) + { + ProcUtils.ProcessStart("xdg-open", path); + } + else if (Utils.IsOSX()) + { + ProcUtils.ProcessStart("open", path); + } + await Task.CompletedTask; + } + + #endregion Setting + + #region core job + + public async Task Reload() + { + //If there are unfinished reload job, marked with next job. + if (!BlReloadEnabled) + { + _hasNextReloadJob = true; + return; + } + + BlReloadEnabled = false; + + await Task.Run(async () => + { + await LoadCore(); + await SysProxyHandler.UpdateSysProxy(_config, false); + await Task.Delay(1000); + }); + Locator.Current.GetService()?.TestServerAvailability(); + + RxApp.MainThreadScheduler.Schedule(() => _ = ReloadResult()); + + BlReloadEnabled = true; + if (_hasNextReloadJob) + { + _hasNextReloadJob = false; + await Reload(); + } + } + + public async Task ReloadResult() + { + // BlReloadEnabled = true; + //Locator.Current.GetService()?.ChangeSystemProxyAsync(_config.systemProxyItem.sysProxyType, false); + ShowClashUI = _config.IsRunningCore(ECoreType.sing_box); + if (ShowClashUI) + { + Locator.Current.GetService()?.ProxiesReload(); + } + else + { + TabMainSelectedIndex = 0; + } + } + + private async Task LoadCore() + { + var node = await ConfigHandler.GetDefaultServer(_config); + await CoreManager.Instance.LoadCore(node); + } + + public async Task CloseCore() + { + await ConfigHandler.SaveConfig(_config); + await CoreManager.Instance.CoreStop(); + } + + private async Task AutoHideStartup() + { + if (_config.UiItem.AutoHideStartup) + { + ShowHideWindow(false); + } + await Task.CompletedTask; + } + + #endregion core job + + #region Presets + + public async Task ApplyRegionalPreset(EPresetType type) + { + await ConfigHandler.ApplyRegionalPreset(_config, type); + await ConfigHandler.InitRouting(_config); + Locator.Current.GetService()?.RefreshRoutingsMenu(); + + await ConfigHandler.SaveConfig(_config); + await new UpdateService().UpdateGeoFileAll(_config, UpdateTaskHandler); + await Reload(); + } + + #endregion Presets +} diff --git a/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs b/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs new file mode 100644 index 00000000..8fc62dfe --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs @@ -0,0 +1,120 @@ +using System.Collections.Concurrent; +using System.Reactive.Linq; +using System.Text.RegularExpressions; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class MsgViewModel : MyReactiveObject +{ + private readonly ConcurrentQueue _queueMsg = new(); + private readonly int _numMaxMsg = 500; + private bool _lastMsgFilterNotAvailable; + private bool _blLockShow = false; + + [Reactive] + public string MsgFilter { get; set; } + + [Reactive] + public bool AutoRefresh { get; set; } + + public MsgViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + MsgFilter = _config.MsgUIItem.MainMsgFilter ?? string.Empty; + AutoRefresh = _config.MsgUIItem.AutoRefresh ?? true; + + this.WhenAnyValue( + x => x.MsgFilter) + .Subscribe(c => DoMsgFilter()); + + this.WhenAnyValue( + x => x.AutoRefresh, + y => y == true) + .Subscribe(c => { _config.MsgUIItem.AutoRefresh = AutoRefresh; }); + + AppEvents.SendMsgViewRequested + .AsObservable() + //.ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async content => await AppendQueueMsg(content)); + } + + private async Task AppendQueueMsg(string msg) + { + //if (msg == Global.CommandClearMsg) + //{ + // ClearMsg(); + // return; + //} + if (AutoRefresh == false) + { + return; + } + _ = EnqueueQueueMsg(msg); + + if (_blLockShow) + { + return; + } + if (!_config.UiItem.ShowInTaskbar) + { + return; + } + + _blLockShow = true; + + await Task.Delay(500); + var txt = string.Join("", _queueMsg.ToArray()); + await _updateView?.Invoke(EViewAction.DispatcherShowMsg, txt); + + _blLockShow = false; + } + + private async Task EnqueueQueueMsg(string msg) + { + //filter msg + if (MsgFilter.IsNotEmpty() && !_lastMsgFilterNotAvailable) + { + try + { + if (!Regex.IsMatch(msg, MsgFilter)) + { + return; + } + } + catch (Exception ex) + { + _queueMsg.Enqueue(ex.Message); + _lastMsgFilterNotAvailable = true; + } + } + + //Enqueue + if (_queueMsg.Count > _numMaxMsg) + { + for (int k = 0; k < _queueMsg.Count - _numMaxMsg; k++) + { + _queueMsg.TryDequeue(out _); + } + } + _queueMsg.Enqueue(msg); + if (!msg.EndsWith(Environment.NewLine)) + { + _queueMsg.Enqueue(Environment.NewLine); + } + await Task.CompletedTask; + } + + public void ClearMsg() + { + _queueMsg.Clear(); + } + + private void DoMsgFilter() + { + _config.MsgUIItem.MainMsgFilter = MsgFilter; + _lastMsgFilterNotAvailable = false; + } +} diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs new file mode 100644 index 00000000..7f446cf2 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs @@ -0,0 +1,426 @@ +using System.Reactive; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class OptionSettingViewModel : MyReactiveObject +{ + #region Core + + [Reactive] public int localPort { get; set; } + [Reactive] public bool SecondLocalPortEnabled { get; set; } + [Reactive] public bool udpEnabled { get; set; } + [Reactive] public bool sniffingEnabled { get; set; } + public IList destOverride { get; set; } + [Reactive] public bool routeOnly { get; set; } + [Reactive] public bool allowLANConn { get; set; } + [Reactive] public bool newPort4LAN { get; set; } + [Reactive] public string user { get; set; } + [Reactive] public string pass { get; set; } + [Reactive] public bool muxEnabled { get; set; } + [Reactive] public bool logEnabled { get; set; } + [Reactive] public string loglevel { get; set; } + [Reactive] public bool defAllowInsecure { get; set; } + [Reactive] public string defFingerprint { get; set; } + [Reactive] public string defUserAgent { get; set; } + [Reactive] public string mux4SboxProtocol { get; set; } + [Reactive] public bool enableCacheFile4Sbox { get; set; } + [Reactive] public int hyUpMbps { get; set; } + [Reactive] public int hyDownMbps { get; set; } + [Reactive] public bool enableFragment { get; set; } + + #endregion Core + + #region Core KCP + + //[Reactive] public int Kcpmtu { get; set; } + //[Reactive] public int Kcptti { get; set; } + //[Reactive] public int KcpuplinkCapacity { get; set; } + //[Reactive] public int KcpdownlinkCapacity { get; set; } + //[Reactive] public int KcpreadBufferSize { get; set; } + //[Reactive] public int KcpwriteBufferSize { get; set; } + //[Reactive] public bool Kcpcongestion { get; set; } + + #endregion Core KCP + + #region UI + + [Reactive] public bool AutoRun { get; set; } + [Reactive] public bool EnableStatistics { get; set; } + [Reactive] public bool KeepOlderDedupl { get; set; } + [Reactive] public bool DisplayRealTimeSpeed { get; set; } + [Reactive] public bool EnableAutoAdjustMainLvColWidth { get; set; } + [Reactive] public bool EnableUpdateSubOnlyRemarksExist { get; set; } + [Reactive] public bool EnableSecurityProtocolTls13 { get; set; } + [Reactive] public bool AutoHideStartup { get; set; } + [Reactive] public bool Hide2TrayWhenClose { get; set; } + [Reactive] public bool EnableDragDropSort { get; set; } + [Reactive] public bool DoubleClick2Activate { get; set; } + [Reactive] public int AutoUpdateInterval { get; set; } + [Reactive] public int TrayMenuServersLimit { get; set; } + [Reactive] public string CurrentFontFamily { get; set; } + [Reactive] public int SpeedTestTimeout { get; set; } + [Reactive] public string SpeedTestUrl { get; set; } + [Reactive] public string SpeedPingTestUrl { get; set; } + [Reactive] public int MixedConcurrencyCount { get; set; } + [Reactive] public bool EnableHWA { get; set; } + [Reactive] public string SubConvertUrl { get; set; } + [Reactive] public int MainGirdOrientation { get; set; } + [Reactive] public string GeoFileSourceUrl { get; set; } + [Reactive] public string SrsFileSourceUrl { get; set; } + [Reactive] public string RoutingRulesSourceUrl { get; set; } + [Reactive] public string IPAPIUrl { get; set; } + + #endregion UI + + #region System proxy + + [Reactive] public bool notProxyLocalAddress { get; set; } + [Reactive] public string systemProxyAdvancedProtocol { get; set; } + [Reactive] public string systemProxyExceptions { get; set; } + + #endregion System proxy + + #region Tun mode + + [Reactive] public bool TunAutoRoute { get; set; } + [Reactive] public bool TunStrictRoute { get; set; } + [Reactive] public string TunStack { get; set; } + [Reactive] public int TunMtu { get; set; } + [Reactive] public bool TunEnableExInbound { get; set; } + [Reactive] public bool TunEnableIPv6Address { get; set; } + + #endregion Tun mode + + #region CoreType + + [Reactive] public string CoreType1 { get; set; } + [Reactive] public string CoreType2 { get; set; } + [Reactive] public string CoreType3 { get; set; } + [Reactive] public string CoreType4 { get; set; } + [Reactive] public string CoreType5 { get; set; } + [Reactive] public string CoreType6 { get; set; } + [Reactive] public string CoreType9 { get; set; } + + #endregion CoreType + + public ReactiveCommand SaveCmd { get; } + + public OptionSettingViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + SaveCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveSettingAsync(); + }); + + _ = Init(); + } + + private async Task Init() + { + await _updateView?.Invoke(EViewAction.InitSettingFont, null); + + #region Core + + var inbound = _config.Inbound.First(); + localPort = inbound.LocalPort; + SecondLocalPortEnabled = inbound.SecondLocalPortEnabled; + udpEnabled = inbound.UdpEnabled; + sniffingEnabled = inbound.SniffingEnabled; + routeOnly = inbound.RouteOnly; + allowLANConn = inbound.AllowLANConn; + newPort4LAN = inbound.NewPort4LAN; + user = inbound.User; + pass = inbound.Pass; + muxEnabled = _config.CoreBasicItem.MuxEnabled; + logEnabled = _config.CoreBasicItem.LogEnabled; + loglevel = _config.CoreBasicItem.Loglevel; + defAllowInsecure = _config.CoreBasicItem.DefAllowInsecure; + defFingerprint = _config.CoreBasicItem.DefFingerprint; + defUserAgent = _config.CoreBasicItem.DefUserAgent; + mux4SboxProtocol = _config.Mux4SboxItem.Protocol; + enableCacheFile4Sbox = _config.CoreBasicItem.EnableCacheFile4Sbox; + hyUpMbps = _config.HysteriaItem.UpMbps; + hyDownMbps = _config.HysteriaItem.DownMbps; + enableFragment = _config.CoreBasicItem.EnableFragment; + + #endregion Core + + #region Core KCP + + //Kcpmtu = _config.kcpItem.mtu; + //Kcptti = _config.kcpItem.tti; + //KcpuplinkCapacity = _config.kcpItem.uplinkCapacity; + //KcpdownlinkCapacity = _config.kcpItem.downlinkCapacity; + //KcpreadBufferSize = _config.kcpItem.readBufferSize; + //KcpwriteBufferSize = _config.kcpItem.writeBufferSize; + //Kcpcongestion = _config.kcpItem.congestion; + + #endregion Core KCP + + #region UI + + AutoRun = _config.GuiItem.AutoRun; + EnableStatistics = _config.GuiItem.EnableStatistics; + DisplayRealTimeSpeed = _config.GuiItem.DisplayRealTimeSpeed; + KeepOlderDedupl = _config.GuiItem.KeepOlderDedupl; + EnableAutoAdjustMainLvColWidth = _config.UiItem.EnableAutoAdjustMainLvColWidth; + EnableUpdateSubOnlyRemarksExist = _config.UiItem.EnableUpdateSubOnlyRemarksExist; + EnableSecurityProtocolTls13 = _config.GuiItem.EnableSecurityProtocolTls13; + AutoHideStartup = _config.UiItem.AutoHideStartup; + Hide2TrayWhenClose = _config.UiItem.Hide2TrayWhenClose; + EnableDragDropSort = _config.UiItem.EnableDragDropSort; + DoubleClick2Activate = _config.UiItem.DoubleClick2Activate; + AutoUpdateInterval = _config.GuiItem.AutoUpdateInterval; + TrayMenuServersLimit = _config.GuiItem.TrayMenuServersLimit; + CurrentFontFamily = _config.UiItem.CurrentFontFamily; + SpeedTestTimeout = _config.SpeedTestItem.SpeedTestTimeout; + SpeedTestUrl = _config.SpeedTestItem.SpeedTestUrl; + MixedConcurrencyCount = _config.SpeedTestItem.MixedConcurrencyCount; + SpeedPingTestUrl = _config.SpeedTestItem.SpeedPingTestUrl; + EnableHWA = _config.GuiItem.EnableHWA; + SubConvertUrl = _config.ConstItem.SubConvertUrl; + MainGirdOrientation = (int)_config.UiItem.MainGirdOrientation; + GeoFileSourceUrl = _config.ConstItem.GeoSourceUrl; + SrsFileSourceUrl = _config.ConstItem.SrsSourceUrl; + RoutingRulesSourceUrl = _config.ConstItem.RouteRulesTemplateSourceUrl; + IPAPIUrl = _config.SpeedTestItem.IPAPIUrl; + + #endregion UI + + #region System proxy + + notProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress; + systemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol; + systemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions; + + #endregion System proxy + + #region Tun mode + + TunAutoRoute = _config.TunModeItem.AutoRoute; + TunStrictRoute = _config.TunModeItem.StrictRoute; + TunStack = _config.TunModeItem.Stack; + TunMtu = _config.TunModeItem.Mtu; + TunEnableExInbound = _config.TunModeItem.EnableExInbound; + TunEnableIPv6Address = _config.TunModeItem.EnableIPv6Address; + + #endregion Tun mode + + await InitCoreType(); + } + + private async Task InitCoreType() + { + if (_config.CoreTypeItem == null) + { + _config.CoreTypeItem = new List(); + } + + foreach (EConfigType it in Enum.GetValues(typeof(EConfigType))) + { + if (_config.CoreTypeItem.FindIndex(t => t.ConfigType == it) >= 0) + { + continue; + } + + _config.CoreTypeItem.Add(new CoreTypeItem() + { + ConfigType = it, + CoreType = ECoreType.Xray + }); + } + _config.CoreTypeItem.ForEach(it => + { + var type = it.CoreType.ToString(); + switch ((int)it.ConfigType) + { + case 1: + CoreType1 = type; + break; + + case 2: + CoreType2 = type; + break; + + case 3: + CoreType3 = type; + break; + + case 4: + CoreType4 = type; + break; + + case 5: + CoreType5 = type; + break; + + case 6: + CoreType6 = type; + break; + + case 9: + CoreType9 = type; + break; + } + }); + await Task.CompletedTask; + } + + private async Task SaveSettingAsync() + { + if (localPort.ToString().IsNullOrEmpty() || !Utils.IsNumeric(localPort.ToString()) + || localPort <= 0 || localPort >= Global.MaxPort) + { + NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort); + return; + } + var needReboot = (EnableStatistics != _config.GuiItem.EnableStatistics + || DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed + || EnableDragDropSort != _config.UiItem.EnableDragDropSort + || EnableHWA != _config.GuiItem.EnableHWA + || CurrentFontFamily != _config.UiItem.CurrentFontFamily + || MainGirdOrientation != (int)_config.UiItem.MainGirdOrientation); + + //if (Utile.IsNullOrEmpty(Kcpmtu.ToString()) || !Utile.IsNumeric(Kcpmtu.ToString()) + // || Utile.IsNullOrEmpty(Kcptti.ToString()) || !Utile.IsNumeric(Kcptti.ToString()) + // || Utile.IsNullOrEmpty(KcpuplinkCapacity.ToString()) || !Utile.IsNumeric(KcpuplinkCapacity.ToString()) + // || Utile.IsNullOrEmpty(KcpdownlinkCapacity.ToString()) || !Utile.IsNumeric(KcpdownlinkCapacity.ToString()) + // || Utile.IsNullOrEmpty(KcpreadBufferSize.ToString()) || !Utile.IsNumeric(KcpreadBufferSize.ToString()) + // || Utile.IsNullOrEmpty(KcpwriteBufferSize.ToString()) || !Utile.IsNumeric(KcpwriteBufferSize.ToString())) + //{ + // NoticeHandler.Instance.Enqueue(ResUI.FillKcpParameters); + // return; + //} + + //Core + _config.Inbound.First().LocalPort = localPort; + _config.Inbound.First().SecondLocalPortEnabled = SecondLocalPortEnabled; + _config.Inbound.First().UdpEnabled = udpEnabled; + _config.Inbound.First().SniffingEnabled = sniffingEnabled; + _config.Inbound.First().DestOverride = destOverride?.ToList(); + _config.Inbound.First().RouteOnly = routeOnly; + _config.Inbound.First().AllowLANConn = allowLANConn; + _config.Inbound.First().NewPort4LAN = newPort4LAN; + _config.Inbound.First().User = user; + _config.Inbound.First().Pass = pass; + if (_config.Inbound.Count > 1) + { + _config.Inbound.RemoveAt(1); + } + _config.CoreBasicItem.LogEnabled = logEnabled; + _config.CoreBasicItem.Loglevel = loglevel; + _config.CoreBasicItem.MuxEnabled = muxEnabled; + _config.CoreBasicItem.DefAllowInsecure = defAllowInsecure; + _config.CoreBasicItem.DefFingerprint = defFingerprint; + _config.CoreBasicItem.DefUserAgent = defUserAgent; + _config.Mux4SboxItem.Protocol = mux4SboxProtocol; + _config.CoreBasicItem.EnableCacheFile4Sbox = enableCacheFile4Sbox; + _config.HysteriaItem.UpMbps = hyUpMbps; + _config.HysteriaItem.DownMbps = hyDownMbps; + _config.CoreBasicItem.EnableFragment = enableFragment; + + _config.GuiItem.AutoRun = AutoRun; + _config.GuiItem.EnableStatistics = EnableStatistics; + _config.GuiItem.DisplayRealTimeSpeed = DisplayRealTimeSpeed; + _config.GuiItem.KeepOlderDedupl = KeepOlderDedupl; + _config.UiItem.EnableAutoAdjustMainLvColWidth = EnableAutoAdjustMainLvColWidth; + _config.UiItem.EnableUpdateSubOnlyRemarksExist = EnableUpdateSubOnlyRemarksExist; + _config.GuiItem.EnableSecurityProtocolTls13 = EnableSecurityProtocolTls13; + _config.UiItem.AutoHideStartup = AutoHideStartup; + _config.UiItem.Hide2TrayWhenClose = Hide2TrayWhenClose; + _config.GuiItem.AutoUpdateInterval = AutoUpdateInterval; + _config.UiItem.EnableDragDropSort = EnableDragDropSort; + _config.UiItem.DoubleClick2Activate = DoubleClick2Activate; + _config.GuiItem.TrayMenuServersLimit = TrayMenuServersLimit; + _config.UiItem.CurrentFontFamily = CurrentFontFamily; + _config.SpeedTestItem.SpeedTestTimeout = SpeedTestTimeout; + _config.SpeedTestItem.MixedConcurrencyCount = MixedConcurrencyCount; + _config.SpeedTestItem.SpeedTestUrl = SpeedTestUrl; + _config.SpeedTestItem.SpeedPingTestUrl = SpeedPingTestUrl; + _config.GuiItem.EnableHWA = EnableHWA; + _config.ConstItem.SubConvertUrl = SubConvertUrl; + _config.UiItem.MainGirdOrientation = (EGirdOrientation)MainGirdOrientation; + _config.ConstItem.GeoSourceUrl = GeoFileSourceUrl; + _config.ConstItem.SrsSourceUrl = SrsFileSourceUrl; + _config.ConstItem.RouteRulesTemplateSourceUrl = RoutingRulesSourceUrl; + _config.SpeedTestItem.IPAPIUrl = IPAPIUrl; + + //systemProxy + _config.SystemProxyItem.SystemProxyExceptions = systemProxyExceptions; + _config.SystemProxyItem.NotProxyLocalAddress = notProxyLocalAddress; + _config.SystemProxyItem.SystemProxyAdvancedProtocol = systemProxyAdvancedProtocol; + + //tun mode + _config.TunModeItem.AutoRoute = TunAutoRoute; + _config.TunModeItem.StrictRoute = TunStrictRoute; + _config.TunModeItem.Stack = TunStack; + _config.TunModeItem.Mtu = TunMtu; + _config.TunModeItem.EnableExInbound = TunEnableExInbound; + _config.TunModeItem.EnableIPv6Address = TunEnableIPv6Address; + + //coreType + await SaveCoreType(); + + if (await ConfigHandler.SaveConfig(_config) == 0) + { + await AutoStartupHandler.UpdateTask(_config); + AppManager.Instance.Reset(); + + NoticeManager.Instance.Enqueue(needReboot ? ResUI.NeedRebootTips : ResUI.OperationSuccess); + _updateView?.Invoke(EViewAction.CloseWindow, null); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } + + private async Task SaveCoreType() + { + for (int k = 1; k <= _config.CoreTypeItem.Count; k++) + { + var item = _config.CoreTypeItem[k - 1]; + var type = string.Empty; + switch ((int)item.ConfigType) + { + case 1: + type = CoreType1; + break; + + case 2: + type = CoreType2; + break; + + case 3: + type = CoreType3; + break; + + case 4: + type = CoreType4; + break; + + case 5: + type = CoreType5; + break; + + case 6: + type = CoreType6; + break; + + case 9: + type = CoreType9; + break; + + default: + continue; + } + item.CoreType = (ECoreType)Enum.Parse(typeof(ECoreType), type); + } + await Task.CompletedTask; + } +} diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs new file mode 100644 index 00000000..8742755e --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs @@ -0,0 +1,352 @@ +using System.Reactive.Linq; +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class ProfilesSelectViewModel : MyReactiveObject +{ + #region private prop + + private string _serverFilter = string.Empty; + private Dictionary _dicHeaderSort = new(); + private string _subIndexId = string.Empty; + + // ConfigType filter state: default include-mode with all types selected + private List _filterConfigTypes = new(); + + private bool _filterExclude = false; + + #endregion private prop + + #region ObservableCollection + + public IObservableCollection ProfileItems { get; } = new ObservableCollectionExtended(); + + public IObservableCollection SubItems { get; } = new ObservableCollectionExtended(); + + [Reactive] + public ProfileItemModel SelectedProfile { get; set; } + + public IList SelectedProfiles { get; set; } + + [Reactive] + public SubItem SelectedSub { get; set; } + + [Reactive] + public string ServerFilter { get; set; } + + // Include/Exclude filter for ConfigType + public List FilterConfigTypes + { + get => _filterConfigTypes; + set => this.RaiseAndSetIfChanged(ref _filterConfigTypes, value); + } + + [Reactive] + public bool FilterExclude + { + get => _filterExclude; + set => this.RaiseAndSetIfChanged(ref _filterExclude, value); + } + + #endregion ObservableCollection + + #region Init + + public ProfilesSelectViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + _subIndexId = _config.SubIndexId ?? string.Empty; + + #region WhenAnyValue && ReactiveCommand + + this.WhenAnyValue( + x => x.SelectedSub, + y => y != null && !y.Remarks.IsNullOrEmpty() && _subIndexId != y.Id) + .Subscribe(async c => await SubSelectedChangedAsync(c)); + + this.WhenAnyValue( + x => x.ServerFilter, + y => y != null && _serverFilter != y) + .Subscribe(async c => await ServerFilterChanged(c)); + + // React to ConfigType filter changes + this.WhenAnyValue(x => x.FilterExclude) + .Skip(1) + .Subscribe(async _ => await RefreshServersBiz()); + + this.WhenAnyValue(x => x.FilterConfigTypes) + .Skip(1) + .Subscribe(async _ => await RefreshServersBiz()); + + #endregion WhenAnyValue && ReactiveCommand + + _ = Init(); + } + + private async Task Init() + { + SelectedProfile = new(); + SelectedSub = new(); + + // Default: include mode with all ConfigTypes selected + try + { + FilterExclude = false; + FilterConfigTypes = Enum.GetValues(typeof(EConfigType)).Cast().ToList(); + } + catch + { + FilterConfigTypes = new(); + } + + await RefreshSubscriptions(); + await RefreshServers(); + } + + #endregion Init + + #region Actions + + public bool CanOk() + { + return SelectedProfile != null && !SelectedProfile.IndexId.IsNullOrEmpty(); + } + + public bool SelectFinish() + { + if (!CanOk()) + { + return false; + } + _updateView?.Invoke(EViewAction.CloseWindow, null); + return true; + } + + #endregion Actions + + #region Servers && Groups + + private async Task SubSelectedChangedAsync(bool c) + { + if (!c) + { + return; + } + _subIndexId = SelectedSub?.Id; + + await RefreshServers(); + + await _updateView?.Invoke(EViewAction.ProfilesFocus, null); + } + + private async Task ServerFilterChanged(bool c) + { + if (!c) + { + return; + } + _serverFilter = ServerFilter; + if (_serverFilter.IsNullOrEmpty()) + { + await RefreshServers(); + } + } + + public async Task RefreshServers() + { + await RefreshServersBiz(); + } + + private async Task RefreshServersBiz() + { + var lstModel = await GetProfileItemsEx(_subIndexId, _serverFilter); + + ProfileItems.Clear(); + ProfileItems.AddRange(lstModel); + if (lstModel.Count > 0) + { + var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId); + if (selected != null) + { + SelectedProfile = selected; + } + else + { + SelectedProfile = lstModel.First(); + } + } + + await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); + } + + public async Task RefreshSubscriptions() + { + SubItems.Clear(); + + SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers }); + + foreach (var item in await AppManager.Instance.SubItems()) + { + SubItems.Add(item); + } + if (_subIndexId != null && SubItems.FirstOrDefault(t => t.Id == _subIndexId) != null) + { + SelectedSub = SubItems.FirstOrDefault(t => t.Id == _subIndexId); + } + else + { + SelectedSub = SubItems.First(); + } + } + + private async Task?> GetProfileItemsEx(string subid, string filter) + { + var lstModel = await AppManager.Instance.ProfileItems(_subIndexId, filter); + lstModel = (from t in lstModel + select new ProfileItemModel + { + IndexId = t.IndexId, + ConfigType = t.ConfigType, + Remarks = t.Remarks, + Address = t.Address, + Port = t.Port, + Security = t.Security, + Network = t.Network, + StreamSecurity = t.StreamSecurity, + Subid = t.Subid, + SubRemarks = t.SubRemarks, + IsActive = t.IndexId == _config.IndexId, + }).OrderBy(t => t.Sort).ToList(); + + // Apply ConfigType filter (include or exclude) + if (FilterConfigTypes != null && FilterConfigTypes.Count > 0) + { + if (FilterExclude) + { + lstModel = lstModel.Where(t => !FilterConfigTypes.Contains(t.ConfigType)).ToList(); + } + else + { + lstModel = lstModel.Where(t => FilterConfigTypes.Contains(t.ConfigType)).ToList(); + } + } + + return lstModel; + } + + public async Task GetProfileItem() + { + if (string.IsNullOrEmpty(SelectedProfile?.IndexId)) + { + return null; + } + var indexId = SelectedProfile.IndexId; + var item = await AppManager.Instance.GetProfileItem(indexId); + if (item is null) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return null; + } + return item; + } + + public async Task?> GetProfileItems() + { + if (SelectedProfiles == null || SelectedProfiles.Count == 0) + { + return null; + } + var lst = new List(); + foreach (var sp in SelectedProfiles) + { + if (string.IsNullOrEmpty(sp?.IndexId)) + { + continue; + } + var item = await AppManager.Instance.GetProfileItem(sp.IndexId); + if (item != null) + { + lst.Add(item); + } + } + if (lst.Count == 0) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return null; + } + return lst; + } + + public void SortServer(string colName) + { + if (colName.IsNullOrEmpty()) + { + return; + } + + var prop = typeof(ProfileItemModel).GetProperty(colName); + if (prop == null) + { + return; + } + + _dicHeaderSort.TryAdd(colName, true); + var asc = _dicHeaderSort[colName]; + + var comparer = Comparer.Create((a, b) => + { + if (ReferenceEquals(a, b)) + { + return 0; + } + if (a is null) + { + return -1; + } + if (b is null) + { + return 1; + } + if (a.GetType() == b.GetType() && a is IComparable ca) + { + return ca.CompareTo(b); + } + return string.Compare(a.ToString(), b.ToString(), StringComparison.OrdinalIgnoreCase); + }); + + object? KeySelector(ProfileItemModel x) + { + return prop.GetValue(x); + } + + IEnumerable sorted = asc + ? ProfileItems.OrderBy(KeySelector, comparer) + : ProfileItems.OrderByDescending(KeySelector, comparer); + + var list = sorted.ToList(); + ProfileItems.Clear(); + ProfileItems.AddRange(list); + + _dicHeaderSort[colName] = !asc; + + return; + } + + #endregion Servers && Groups + + #region Public API + + // External setter for ConfigType filter + public void SetConfigTypeFilter(IEnumerable types, bool exclude = false) + { + FilterConfigTypes = types?.Distinct().ToList() ?? new List(); + FilterExclude = exclude; + } + + #endregion Public API +} diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs new file mode 100644 index 00000000..14216fee --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -0,0 +1,856 @@ +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Text; +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Splat; + +namespace ServiceLib.ViewModels; + +public class ProfilesViewModel : MyReactiveObject +{ + #region private prop + + private List _lstProfile; + private string _serverFilter = string.Empty; + private Dictionary _dicHeaderSort = new(); + private SpeedtestService? _speedtestService; + + #endregion private prop + + #region ObservableCollection + + public IObservableCollection ProfileItems { get; } = new ObservableCollectionExtended(); + + public IObservableCollection SubItems { get; } = new ObservableCollectionExtended(); + + [Reactive] + public ProfileItemModel SelectedProfile { get; set; } + + public IList SelectedProfiles { get; set; } + + [Reactive] + public SubItem SelectedSub { get; set; } + + [Reactive] + public SubItem SelectedMoveToGroup { get; set; } + + [Reactive] + public string ServerFilter { get; set; } + + #endregion ObservableCollection + + #region Menu + + //servers delete + public ReactiveCommand EditServerCmd { get; } + + public ReactiveCommand RemoveServerCmd { get; } + public ReactiveCommand RemoveDuplicateServerCmd { get; } + public ReactiveCommand CopyServerCmd { get; } + public ReactiveCommand SetDefaultServerCmd { get; } + public ReactiveCommand ShareServerCmd { get; } + public ReactiveCommand SetDefaultMultipleServerXrayRandomCmd { get; } + public ReactiveCommand SetDefaultMultipleServerXrayRoundRobinCmd { get; } + public ReactiveCommand SetDefaultMultipleServerXrayLeastPingCmd { get; } + public ReactiveCommand SetDefaultMultipleServerXrayLeastLoadCmd { get; } + public ReactiveCommand SetDefaultMultipleServerSingBoxLeastPingCmd { get; } + + //servers move + public ReactiveCommand MoveTopCmd { get; } + + public ReactiveCommand MoveUpCmd { get; } + public ReactiveCommand MoveDownCmd { get; } + public ReactiveCommand MoveBottomCmd { get; } + + //servers ping + public ReactiveCommand MixedTestServerCmd { get; } + + public ReactiveCommand TcpingServerCmd { get; } + public ReactiveCommand RealPingServerCmd { get; } + public ReactiveCommand SpeedServerCmd { get; } + public ReactiveCommand SortServerResultCmd { get; } + public ReactiveCommand RemoveInvalidServerResultCmd { get; } + + //servers export + public ReactiveCommand Export2ClientConfigCmd { get; } + + public ReactiveCommand Export2ClientConfigClipboardCmd { get; } + public ReactiveCommand Export2ShareUrlCmd { get; } + public ReactiveCommand Export2ShareUrlBase64Cmd { get; } + + public ReactiveCommand AddSubCmd { get; } + public ReactiveCommand EditSubCmd { get; } + + #endregion Menu + + #region Init + + public ProfilesViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + #region WhenAnyValue && ReactiveCommand + + var canEditRemove = this.WhenAnyValue( + x => x.SelectedProfile, + selectedSource => selectedSource != null && !selectedSource.IndexId.IsNullOrEmpty()); + + this.WhenAnyValue( + x => x.SelectedSub, + y => y != null && !y.Remarks.IsNullOrEmpty() && _config.SubIndexId != y.Id) + .Subscribe(async c => await SubSelectedChangedAsync(c)); + this.WhenAnyValue( + x => x.SelectedMoveToGroup, + y => y != null && !y.Remarks.IsNullOrEmpty()) + .Subscribe(async c => await MoveToGroup(c)); + + this.WhenAnyValue( + x => x.ServerFilter, + y => y != null && _serverFilter != y) + .Subscribe(async c => await ServerFilterChanged(c)); + + //servers delete + EditServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await EditServerAsync(EConfigType.Custom); + }, canEditRemove); + RemoveServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await RemoveServerAsync(); + }, canEditRemove); + RemoveDuplicateServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await RemoveDuplicateServer(); + }); + CopyServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await CopyServer(); + }, canEditRemove); + SetDefaultServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await SetDefaultServer(); + }, canEditRemove); + ShareServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await ShareServerAsync(); + }, canEditRemove); + SetDefaultMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () => + { + await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.Random); + }, canEditRemove); + SetDefaultMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () => + { + await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin); + }, canEditRemove); + SetDefaultMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () => + { + await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing); + }, canEditRemove); + SetDefaultMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () => + { + await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad); + }, canEditRemove); + SetDefaultMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () => + { + await SetDefaultMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing); + }, canEditRemove); + + //servers move + MoveTopCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Top); + }, canEditRemove); + MoveUpCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Up); + }, canEditRemove); + MoveDownCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Down); + }, canEditRemove); + MoveBottomCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Bottom); + }, canEditRemove); + + //servers ping + MixedTestServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await ServerSpeedtest(ESpeedActionType.Mixedtest); + }); + TcpingServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await ServerSpeedtest(ESpeedActionType.Tcping); + }, canEditRemove); + RealPingServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await ServerSpeedtest(ESpeedActionType.Realping); + }, canEditRemove); + SpeedServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await ServerSpeedtest(ESpeedActionType.Speedtest); + }, canEditRemove); + SortServerResultCmd = ReactiveCommand.CreateFromTask(async () => + { + await SortServer(EServerColName.DelayVal.ToString()); + }); + RemoveInvalidServerResultCmd = ReactiveCommand.CreateFromTask(async () => + { + await RemoveInvalidServerResult(); + }); + //servers export + Export2ClientConfigCmd = ReactiveCommand.CreateFromTask(async () => + { + await Export2ClientConfigAsync(false); + }, canEditRemove); + Export2ClientConfigClipboardCmd = ReactiveCommand.CreateFromTask(async () => + { + await Export2ClientConfigAsync(true); + }, canEditRemove); + Export2ShareUrlCmd = ReactiveCommand.CreateFromTask(async () => + { + await Export2ShareUrlAsync(false); + }, canEditRemove); + Export2ShareUrlBase64Cmd = ReactiveCommand.CreateFromTask(async () => + { + await Export2ShareUrlAsync(true); + }, canEditRemove); + + //Subscription + AddSubCmd = ReactiveCommand.CreateFromTask(async () => + { + await EditSubAsync(true); + }); + EditSubCmd = ReactiveCommand.CreateFromTask(async () => + { + await EditSubAsync(false); + }); + + #endregion WhenAnyValue && ReactiveCommand + + #region AppEvents + + AppEvents.ProfilesRefreshRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await RefreshServersBiz()); + + AppEvents.DispatcherStatisticsRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async result => await UpdateStatistics(result)); + + #endregion AppEvents + + _ = Init(); + } + + private async Task Init() + { + SelectedProfile = new(); + SelectedSub = new(); + SelectedMoveToGroup = new(); + + await RefreshSubscriptions(); + //await RefreshServers(); + } + + #endregion Init + + #region Actions + + private void Reload() + { + Locator.Current.GetService()?.Reload(); + } + + public async Task SetSpeedTestResult(SpeedTestResult result) + { + if (result.IndexId.IsNullOrEmpty()) + { + NoticeManager.Instance.SendMessageEx(result.Delay); + NoticeManager.Instance.Enqueue(result.Delay); + return; + } + var item = ProfileItems.FirstOrDefault(it => it.IndexId == result.IndexId); + if (item == null) + { + return; + } + + if (result.Delay.IsNotEmpty()) + { + int.TryParse(result.Delay, out var temp); + item.Delay = temp; + item.DelayVal = result.Delay ?? string.Empty; + } + if (result.Speed.IsNotEmpty()) + { + item.SpeedVal = result.Speed ?? string.Empty; + } + //_profileItems.Replace(item, JsonUtils.DeepCopy(item)); + } + + public async Task UpdateStatistics(ServerSpeedItem update) + { + if (!_config.GuiItem.EnableStatistics + || (update.ProxyUp + update.ProxyDown) <= 0 + || DateTime.Now.Second % 3 != 0) + { + return; + } + + try + { + var item = ProfileItems.FirstOrDefault(it => it.IndexId == update.IndexId); + if (item != null) + { + item.TodayDown = Utils.HumanFy(update.TodayDown); + item.TodayUp = Utils.HumanFy(update.TodayUp); + item.TotalDown = Utils.HumanFy(update.TotalDown); + item.TotalUp = Utils.HumanFy(update.TotalUp); + + //if (SelectedProfile?.IndexId == item.IndexId) + //{ + // var temp = JsonUtils.DeepCopy(item); + // _profileItems.Replace(item, temp); + // SelectedProfile = temp; + //} + //else + //{ + // _profileItems.Replace(item, JsonUtils.DeepCopy(item)); + //} + } + } + catch + { + } + } + + #endregion Actions + + #region Servers && Groups + + private async Task SubSelectedChangedAsync(bool c) + { + if (!c) + { + return; + } + _config.SubIndexId = SelectedSub?.Id; + + await RefreshServers(); + + await _updateView?.Invoke(EViewAction.ProfilesFocus, null); + } + + private async Task ServerFilterChanged(bool c) + { + if (!c) + { + return; + } + _serverFilter = ServerFilter; + if (_serverFilter.IsNullOrEmpty()) + { + await RefreshServers(); + } + } + + public async Task RefreshServers() + { + AppEvents.ProfilesRefreshRequested.OnNext(Unit.Default); + + await Task.Delay(200); + } + + private async Task RefreshServersBiz() + { + var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter); + _lstProfile = JsonUtils.Deserialize>(JsonUtils.Serialize(lstModel)) ?? []; + + ProfileItems.Clear(); + ProfileItems.AddRange(lstModel); + if (lstModel.Count > 0) + { + var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId); + if (selected != null) + { + SelectedProfile = selected; + } + else + { + SelectedProfile = lstModel.First(); + } + } + + await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); + } + + public async Task RefreshSubscriptions() + { + SubItems.Clear(); + + SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers }); + + foreach (var item in await AppManager.Instance.SubItems()) + { + SubItems.Add(item); + } + if (_config.SubIndexId != null && SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId) != null) + { + SelectedSub = SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId); + } + else + { + SelectedSub = SubItems.First(); + } + } + + private async Task?> GetProfileItemsEx(string subid, string filter) + { + var lstModel = await AppManager.Instance.ProfileItems(_config.SubIndexId, filter); + + await ConfigHandler.SetDefaultServer(_config, lstModel); + + var lstServerStat = (_config.GuiItem.EnableStatistics ? StatisticsManager.Instance.ServerStat : null) ?? []; + var lstProfileExs = await ProfileExManager.Instance.GetProfileExs(); + lstModel = (from t in lstModel + join t2 in lstServerStat on t.IndexId equals t2.IndexId into t2b + from t22 in t2b.DefaultIfEmpty() + join t3 in lstProfileExs on t.IndexId equals t3.IndexId into t3b + from t33 in t3b.DefaultIfEmpty() + select new ProfileItemModel + { + IndexId = t.IndexId, + ConfigType = t.ConfigType, + Remarks = t.Remarks, + Address = t.Address, + Port = t.Port, + Security = t.Security, + Network = t.Network, + StreamSecurity = t.StreamSecurity, + Subid = t.Subid, + SubRemarks = t.SubRemarks, + IsActive = t.IndexId == _config.IndexId, + Sort = t33?.Sort ?? 0, + Delay = t33?.Delay ?? 0, + Speed = t33?.Speed ?? 0, + DelayVal = t33?.Delay != 0 ? $"{t33?.Delay}" : string.Empty, + SpeedVal = t33?.Speed > 0 ? $"{t33?.Speed}" : t33?.Message ?? string.Empty, + TodayDown = t22 == null ? "" : Utils.HumanFy(t22.TodayDown), + TodayUp = t22 == null ? "" : Utils.HumanFy(t22.TodayUp), + TotalDown = t22 == null ? "" : Utils.HumanFy(t22.TotalDown), + TotalUp = t22 == null ? "" : Utils.HumanFy(t22.TotalUp) + }).OrderBy(t => t.Sort).ToList(); + + return lstModel; + } + + #endregion Servers && Groups + + #region Add Servers + + private async Task?> GetProfileItems(bool latest) + { + var lstSelected = new List(); + if (SelectedProfiles == null || SelectedProfiles.Count <= 0) + { + return null; + } + + var orderProfiles = SelectedProfiles?.OrderBy(t => t.Sort); + if (latest) + { + foreach (var profile in orderProfiles) + { + var item = await AppManager.Instance.GetProfileItem(profile.IndexId); + if (item is not null) + { + lstSelected.Add(item); + } + } + } + else + { + lstSelected = JsonUtils.Deserialize>(JsonUtils.Serialize(orderProfiles)); + } + + return lstSelected; + } + + public async Task EditServerAsync(EConfigType eConfigType) + { + if (string.IsNullOrEmpty(SelectedProfile?.IndexId)) + { + return; + } + var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId); + if (item is null) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return; + } + eConfigType = item.ConfigType; + + bool? ret = false; + if (eConfigType == EConfigType.Custom) + { + ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); + } + else + { + ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); + } + if (ret == true) + { + await RefreshServers(); + if (item.IndexId == _config.IndexId) + { + Reload(); + } + } + } + + public async Task RemoveServerAsync() + { + var lstSelected = await GetProfileItems(true); + if (lstSelected == null) + { + return; + } + if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) + { + return; + } + var exists = lstSelected.Exists(t => t.IndexId == _config.IndexId); + + await ConfigHandler.RemoveServers(_config, lstSelected); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + if (lstSelected.Count == ProfileItems.Count) + { + ProfileItems.Clear(); + } + await RefreshServers(); + if (exists) + { + Reload(); + } + } + + private async Task RemoveDuplicateServer() + { + var tuple = await ConfigHandler.DedupServerList(_config, _config.SubIndexId); + if (tuple.Item1 > 0 || tuple.Item2 > 0) + { + await RefreshServers(); + Reload(); + } + NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveDuplicateServerResult, tuple.Item1, tuple.Item2)); + } + + private async Task CopyServer() + { + var lstSelected = await GetProfileItems(false); + if (lstSelected == null) + { + return; + } + if (await ConfigHandler.CopyServer(_config, lstSelected) == 0) + { + await RefreshServers(); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + } + } + + public async Task SetDefaultServer() + { + if (string.IsNullOrEmpty(SelectedProfile?.IndexId)) + { + return; + } + await SetDefaultServer(SelectedProfile.IndexId); + } + + public async Task SetDefaultServer(string? indexId) + { + if (indexId.IsNullOrEmpty()) + { + return; + } + if (indexId == _config.IndexId) + { + return; + } + var item = await AppManager.Instance.GetProfileItem(indexId); + if (item is null) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return; + } + + if (await ConfigHandler.SetDefaultServerIndex(_config, indexId) == 0) + { + await RefreshServers(); + Reload(); + } + } + + public async Task ShareServerAsync() + { + var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId); + if (item is null) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return; + } + var url = FmtHandler.GetShareUri(item); + if (url.IsNullOrEmpty()) + { + return; + } + + await _updateView?.Invoke(EViewAction.ShareServer, url); + } + + private async Task SetDefaultMultipleServer(ECoreType coreType, EMultipleLoad multipleLoad) + { + var lstSelected = await GetProfileItems(true); + if (lstSelected == null) + { + return; + } + + var ret = await ConfigHandler.AddCustomServer4Multiple(_config, lstSelected, coreType, multipleLoad); + if (ret.Success != true) + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + return; + } + if (ret?.Data?.ToString() == _config.IndexId) + { + await RefreshServers(); + Reload(); + } + else + { + await SetDefaultServer(ret?.Data?.ToString()); + } + } + + public async Task SortServer(string colName) + { + if (colName.IsNullOrEmpty()) + { + return; + } + + _dicHeaderSort.TryAdd(colName, true); + _dicHeaderSort.TryGetValue(colName, out bool asc); + if (await ConfigHandler.SortServers(_config, _config.SubIndexId, colName, asc) != 0) + { + return; + } + _dicHeaderSort[colName] = !asc; + await RefreshServers(); + } + + public async Task RemoveInvalidServerResult() + { + var count = await ConfigHandler.RemoveInvalidServerResult(_config, _config.SubIndexId); + await RefreshServers(); + NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveInvalidServerResultTip, count)); + } + + //move server + private async Task MoveToGroup(bool c) + { + if (!c) + { + return; + } + + var lstSelected = await GetProfileItems(true); + if (lstSelected == null) + { + return; + } + + await ConfigHandler.MoveToGroup(_config, lstSelected, SelectedMoveToGroup.Id); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + + await RefreshServers(); + SelectedMoveToGroup = null; + SelectedMoveToGroup = new(); + } + + public async Task MoveServer(EMove eMove) + { + var item = _lstProfile.FirstOrDefault(t => t.IndexId == SelectedProfile.IndexId); + if (item is null) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return; + } + + var index = _lstProfile.IndexOf(item); + if (index < 0) + { + return; + } + if (await ConfigHandler.MoveServer(_config, _lstProfile, index, eMove) == 0) + { + await RefreshServers(); + } + } + + public async Task MoveServerTo(int startIndex, ProfileItemModel targetItem) + { + var targetIndex = ProfileItems.IndexOf(targetItem); + if (startIndex >= 0 && targetIndex >= 0 && startIndex != targetIndex) + { + if (await ConfigHandler.MoveServer(_config, _lstProfile, startIndex, EMove.Position, targetIndex) == 0) + { + await RefreshServers(); + } + } + } + + public async Task ServerSpeedtest(ESpeedActionType actionType) + { + if (actionType == ESpeedActionType.Mixedtest) + { + SelectedProfiles = ProfileItems; + } + var lstSelected = await GetProfileItems(false); + if (lstSelected == null) + { + return; + } + + _speedtestService ??= new SpeedtestService(_config, async (SpeedTestResult result) => + { + RxApp.MainThreadScheduler.Schedule(result, (scheduler, result) => + { + _ = SetSpeedTestResult(result); + return Disposable.Empty; + }); + }); + _speedtestService?.RunLoop(actionType, lstSelected); + } + + public void ServerSpeedtestStop() + { + _speedtestService?.ExitLoop(); + } + + private async Task Export2ClientConfigAsync(bool blClipboard) + { + var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId); + if (item is null) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return; + } + if (blClipboard) + { + var result = await CoreConfigHandler.GenerateClientConfig(item, null); + if (result.Success != true) + { + NoticeManager.Instance.Enqueue(result.Msg); + } + else + { + await _updateView?.Invoke(EViewAction.SetClipboardData, result.Data); + NoticeManager.Instance.SendMessage(ResUI.OperationSuccess); + } + } + else + { + await _updateView?.Invoke(EViewAction.SaveFileDialog, item); + } + } + + public async Task Export2ClientConfigResult(string fileName, ProfileItem item) + { + if (fileName.IsNullOrEmpty()) + { + return; + } + var result = await CoreConfigHandler.GenerateClientConfig(item, fileName); + if (result.Success != true) + { + NoticeManager.Instance.Enqueue(result.Msg); + } + else + { + NoticeManager.Instance.SendMessageAndEnqueue(string.Format(ResUI.SaveClientConfigurationIn, fileName)); + } + } + + public async Task Export2ShareUrlAsync(bool blEncode) + { + var lstSelected = await GetProfileItems(true); + if (lstSelected == null) + { + return; + } + + StringBuilder sb = new(); + foreach (var it in lstSelected) + { + var url = FmtHandler.GetShareUri(it); + if (url.IsNullOrEmpty()) + { + continue; + } + sb.Append(url); + sb.AppendLine(); + } + if (sb.Length > 0) + { + if (blEncode) + { + await _updateView?.Invoke(EViewAction.SetClipboardData, Utils.Base64Encode(sb.ToString())); + } + else + { + await _updateView?.Invoke(EViewAction.SetClipboardData, sb.ToString()); + } + NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully); + } + } + + #endregion Add Servers + + #region Subscription + + private async Task EditSubAsync(bool blNew) + { + SubItem item; + if (blNew) + { + item = new(); + } + else + { + item = await AppManager.Instance.GetSubItem(_config.SubIndexId); + if (item is null) + { + return; + } + } + if (await _updateView?.Invoke(EViewAction.SubEditWindow, item) == true) + { + await RefreshSubscriptions(); + await SubSelectedChangedAsync(true); + } + } + + #endregion Subscription +} diff --git a/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs new file mode 100644 index 00000000..758aa8fe --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs @@ -0,0 +1,92 @@ +using System.Reactive; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class RoutingRuleDetailsViewModel : MyReactiveObject +{ + public IList ProtocolItems { get; set; } + public IList InboundTagItems { get; set; } + + [Reactive] + public RulesItem SelectedSource { get; set; } + + [Reactive] + public string Domain { get; set; } + + [Reactive] + public string IP { get; set; } + + [Reactive] + public string Process { get; set; } + + [Reactive] + public bool AutoSort { get; set; } + + public ReactiveCommand SaveCmd { get; } + + public RoutingRuleDetailsViewModel(RulesItem rulesItem, Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + SaveCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveRulesAsync(); + }); + + if (rulesItem.Id.IsNullOrEmpty()) + { + rulesItem.Id = Utils.GetGuid(false); + rulesItem.OutboundTag = Global.ProxyTag; + rulesItem.Enabled = true; + SelectedSource = rulesItem; + } + else + { + SelectedSource = rulesItem; + } + + Domain = Utils.List2String(SelectedSource.Domain, true); + IP = Utils.List2String(SelectedSource.Ip, true); + Process = Utils.List2String(SelectedSource.Process, true); + } + + private async Task SaveRulesAsync() + { + Domain = Utils.Convert2Comma(Domain); + IP = Utils.Convert2Comma(IP); + Process = Utils.Convert2Comma(Process); + + if (AutoSort) + { + SelectedSource.Domain = Utils.String2ListSorted(Domain); + SelectedSource.Ip = Utils.String2ListSorted(IP); + SelectedSource.Process = Utils.String2ListSorted(Process); + } + else + { + SelectedSource.Domain = Utils.String2List(Domain); + SelectedSource.Ip = Utils.String2List(IP); + SelectedSource.Process = Utils.String2List(Process); + } + SelectedSource.Protocol = ProtocolItems?.ToList(); + SelectedSource.InboundTag = InboundTagItems?.ToList(); + + var hasRule = SelectedSource.Domain?.Count > 0 + || SelectedSource.Ip?.Count > 0 + || SelectedSource.Protocol?.Count > 0 + || SelectedSource.Process?.Count > 0 + || SelectedSource.Port.IsNotEmpty() + || SelectedSource.Network.IsNotEmpty(); + + if (!hasRule) + { + NoticeManager.Instance.Enqueue(string.Format(ResUI.RoutingRuleDetailRequiredTips, "Network/Port/Protocol/Domain/IP/Process")); + return; + } + //NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); + await _updateView?.Invoke(EViewAction.CloseWindow, null); + } +} diff --git a/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs new file mode 100644 index 00000000..4b192f04 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs @@ -0,0 +1,339 @@ +using System.Reactive; +using System.Text.Json; +using System.Text.Json.Serialization; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class RoutingRuleSettingViewModel : MyReactiveObject +{ + private List _rules; + + [Reactive] + public RoutingItem SelectedRouting { get; set; } + + public IObservableCollection RulesItems { get; } = new ObservableCollectionExtended(); + + [Reactive] + public RulesItemModel SelectedSource { get; set; } + + public IList SelectedSources { get; set; } + + public ReactiveCommand RuleAddCmd { get; } + public ReactiveCommand ImportRulesFromFileCmd { get; } + public ReactiveCommand ImportRulesFromClipboardCmd { get; } + public ReactiveCommand ImportRulesFromUrlCmd { get; } + public ReactiveCommand RuleRemoveCmd { get; } + public ReactiveCommand RuleExportSelectedCmd { get; } + public ReactiveCommand MoveTopCmd { get; } + public ReactiveCommand MoveUpCmd { get; } + public ReactiveCommand MoveDownCmd { get; } + public ReactiveCommand MoveBottomCmd { get; } + + public ReactiveCommand SaveCmd { get; } + + public RoutingRuleSettingViewModel(RoutingItem routingItem, Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + var canEditRemove = this.WhenAnyValue( + x => x.SelectedSource, + selectedSource => selectedSource != null && !selectedSource.OutboundTag.IsNullOrEmpty()); + + RuleAddCmd = ReactiveCommand.CreateFromTask(async () => + { + await RuleEditAsync(true); + }); + ImportRulesFromFileCmd = ReactiveCommand.CreateFromTask(async () => + { + await _updateView?.Invoke(EViewAction.ImportRulesFromFile, null); + }); + ImportRulesFromClipboardCmd = ReactiveCommand.CreateFromTask(async () => + { + await ImportRulesFromClipboardAsync(null); + }); + ImportRulesFromUrlCmd = ReactiveCommand.CreateFromTask(async () => + { + await ImportRulesFromUrl(); + }); + + RuleRemoveCmd = ReactiveCommand.CreateFromTask(async () => + { + await RuleRemoveAsync(); + }, canEditRemove); + RuleExportSelectedCmd = ReactiveCommand.CreateFromTask(async () => + { + await RuleExportSelectedAsync(); + }, canEditRemove); + + MoveTopCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveRule(EMove.Top); + }, canEditRemove); + MoveUpCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveRule(EMove.Up); + }, canEditRemove); + MoveDownCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveRule(EMove.Down); + }, canEditRemove); + MoveBottomCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveRule(EMove.Bottom); + }, canEditRemove); + + SaveCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveRoutingAsync(); + }); + + SelectedSource = new(); + SelectedRouting = routingItem; + _rules = routingItem.Id.IsNullOrEmpty() ? new() : JsonUtils.Deserialize>(SelectedRouting.RuleSet); + + RefreshRulesItems(); + } + + public void RefreshRulesItems() + { + RulesItems.Clear(); + + foreach (var item in _rules) + { + var it = new RulesItemModel() + { + Id = item.Id, + OutboundTag = item.OutboundTag, + Port = item.Port, + Network = item.Network, + Protocols = Utils.List2String(item.Protocol), + InboundTags = Utils.List2String(item.InboundTag), + Domains = Utils.List2String(item.Domain), + Ips = Utils.List2String(item.Ip), + Enabled = item.Enabled, + Remarks = item.Remarks, + }; + RulesItems.Add(it); + } + } + + public async Task RuleEditAsync(bool blNew) + { + RulesItem? item; + if (blNew) + { + item = new(); + } + else + { + item = _rules.FirstOrDefault(t => t.Id == SelectedSource?.Id); + if (item is null) + { + return; + } + } + if (await _updateView?.Invoke(EViewAction.RoutingRuleDetailsWindow, item) == true) + { + if (blNew) + { + _rules.Insert(0, item); + } + RefreshRulesItems(); + } + } + + public async Task RuleRemoveAsync() + { + if (SelectedSource is null || SelectedSource.OutboundTag.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); + return; + } + if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) + { + return; + } + foreach (var it in SelectedSources ?? [SelectedSource]) + { + var item = _rules.FirstOrDefault(t => t.Id == it?.Id); + if (item != null) + { + _rules.Remove(item); + } + } + + RefreshRulesItems(); + } + + public async Task RuleExportSelectedAsync() + { + if (SelectedSource is null || SelectedSource.OutboundTag.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); + return; + } + + var lst = new List(); + var sources = SelectedSources ?? [SelectedSource]; + foreach (var it in _rules) + { + if (sources.Any(t => t.Id == it?.Id)) + { + var item2 = JsonUtils.DeepCopy(it); + item2.Id = null; + lst.Add(item2 ?? new()); + } + } + if (lst.Count > 0) + { + var options = new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + await _updateView?.Invoke(EViewAction.SetClipboardData, JsonUtils.Serialize(lst, options)); + } + } + + public async Task MoveRule(EMove eMove) + { + if (SelectedSource is null || SelectedSource.OutboundTag.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); + return; + } + + var item = _rules.FirstOrDefault(t => t.Id == SelectedSource?.Id); + if (item == null) + { + return; + } + var index = _rules.IndexOf(item); + if (await ConfigHandler.MoveRoutingRule(_rules, index, eMove) == 0) + { + RefreshRulesItems(); + } + } + + private async Task SaveRoutingAsync() + { + string remarks = SelectedRouting.Remarks; + if (remarks.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); + return; + } + var item = SelectedRouting; + foreach (var it in _rules) + { + it.Id = Utils.GetGuid(false); + } + item.RuleNum = _rules.Count; + item.RuleSet = JsonUtils.Serialize(_rules, false); + + if (await ConfigHandler.SaveRoutingItem(_config, item) == 0) + { + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + _updateView?.Invoke(EViewAction.CloseWindow, null); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } + + #region Import rules + + public async Task ImportRulesFromFileAsync(string fileName) + { + if (fileName.IsNullOrEmpty()) + { + return; + } + + var result = EmbedUtils.LoadResource(fileName); + if (result.IsNullOrEmpty()) + { + return; + } + var ret = await AddBatchRoutingRulesAsync(SelectedRouting, result); + if (ret == 0) + { + RefreshRulesItems(); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + } + } + + public async Task ImportRulesFromClipboardAsync(string? clipboardData) + { + if (clipboardData == null) + { + await _updateView?.Invoke(EViewAction.ImportRulesFromClipboard, null); + return; + } + var ret = await AddBatchRoutingRulesAsync(SelectedRouting, clipboardData); + if (ret == 0) + { + RefreshRulesItems(); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + } + } + + private async Task ImportRulesFromUrl() + { + var url = SelectedRouting.Url; + if (url.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.MsgNeedUrl); + return; + } + + DownloadService downloadHandle = new DownloadService(); + var result = await downloadHandle.TryDownloadString(url, true, ""); + var ret = await AddBatchRoutingRulesAsync(SelectedRouting, result); + if (ret == 0) + { + RefreshRulesItems(); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + } + } + + private async Task AddBatchRoutingRulesAsync(RoutingItem routingItem, string? clipboardData) + { + bool blReplace = false; + if (await _updateView?.Invoke(EViewAction.AddBatchRoutingRulesYesNo, null) == false) + { + blReplace = true; + } + if (clipboardData.IsNullOrEmpty()) + { + return -1; + } + var lstRules = JsonUtils.Deserialize>(clipboardData); + if (lstRules == null) + { + return -1; + } + foreach (var rule in lstRules) + { + rule.Id = Utils.GetGuid(false); + } + + if (blReplace) + { + _rules = lstRules; + } + else + { + _rules.AddRange(lstRules); + } + return 0; + } + + #endregion Import rules +} diff --git a/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs new file mode 100644 index 00000000..5237a8d2 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs @@ -0,0 +1,192 @@ +using System.Reactive; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class RoutingSettingViewModel : MyReactiveObject +{ + #region Reactive + + public IObservableCollection RoutingItems { get; } = new ObservableCollectionExtended(); + + [Reactive] + public RoutingItemModel SelectedSource { get; set; } + + public IList SelectedSources { get; set; } + + [Reactive] + public string DomainStrategy { get; set; } + + [Reactive] + public string DomainStrategy4Singbox { get; set; } + + public ReactiveCommand RoutingAdvancedAddCmd { get; } + public ReactiveCommand RoutingAdvancedRemoveCmd { get; } + public ReactiveCommand RoutingAdvancedSetDefaultCmd { get; } + public ReactiveCommand RoutingAdvancedImportRulesCmd { get; } + + public ReactiveCommand SaveCmd { get; } + public bool IsModified { get; set; } + + #endregion Reactive + + public RoutingSettingViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + var canEditRemove = this.WhenAnyValue( + x => x.SelectedSource, + selectedSource => selectedSource != null && !selectedSource.Remarks.IsNullOrEmpty()); + + RoutingAdvancedAddCmd = ReactiveCommand.CreateFromTask(async () => + { + await RoutingAdvancedEditAsync(true); + }); + RoutingAdvancedRemoveCmd = ReactiveCommand.CreateFromTask(async () => + { + await RoutingAdvancedRemoveAsync(); + }, canEditRemove); + RoutingAdvancedSetDefaultCmd = ReactiveCommand.CreateFromTask(async () => + { + await RoutingAdvancedSetDefault(); + }, canEditRemove); + RoutingAdvancedImportRulesCmd = ReactiveCommand.CreateFromTask(async () => + { + await RoutingAdvancedImportRules(); + }); + + SaveCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveRoutingAsync(); + }); + + _ = Init(); + } + + private async Task Init() + { + SelectedSource = new(); + + DomainStrategy = _config.RoutingBasicItem.DomainStrategy; + DomainStrategy4Singbox = _config.RoutingBasicItem.DomainStrategy4Singbox; + + await ConfigHandler.InitBuiltinRouting(_config); + await RefreshRoutingItems(); + } + + #region Refresh Save + + public async Task RefreshRoutingItems() + { + RoutingItems.Clear(); + + var routings = await AppManager.Instance.RoutingItems(); + foreach (var item in routings) + { + var it = new RoutingItemModel() + { + IsActive = item.IsActive, + RuleNum = item.RuleNum, + Id = item.Id, + Remarks = item.Remarks, + Url = item.Url, + CustomIcon = item.CustomIcon, + CustomRulesetPath4Singbox = item.CustomRulesetPath4Singbox, + Sort = item.Sort, + }; + RoutingItems.Add(it); + } + } + + private async Task SaveRoutingAsync() + { + _config.RoutingBasicItem.DomainStrategy = DomainStrategy; + _config.RoutingBasicItem.DomainStrategy4Singbox = DomainStrategy4Singbox; + + if (await ConfigHandler.SaveConfig(_config) == 0) + { + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + _updateView?.Invoke(EViewAction.CloseWindow, null); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } + + #endregion Refresh Save + + public async Task RoutingAdvancedEditAsync(bool blNew) + { + RoutingItem item; + if (blNew) + { + item = new(); + } + else + { + item = await AppManager.Instance.GetRoutingItem(SelectedSource?.Id); + if (item is null) + { + return; + } + } + if (await _updateView?.Invoke(EViewAction.RoutingRuleSettingWindow, item) == true) + { + await RefreshRoutingItems(); + IsModified = true; + } + } + + public async Task RoutingAdvancedRemoveAsync() + { + if (SelectedSource is null || SelectedSource.Remarks.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); + return; + } + if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) + { + return; + } + foreach (var it in SelectedSources ?? [SelectedSource]) + { + var item = await AppManager.Instance.GetRoutingItem(it?.Id); + if (item != null) + { + await ConfigHandler.RemoveRoutingItem(item); + } + } + + await RefreshRoutingItems(); + IsModified = true; + } + + public async Task RoutingAdvancedSetDefault() + { + var item = await AppManager.Instance.GetRoutingItem(SelectedSource?.Id); + if (item is null) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); + return; + } + + if (await ConfigHandler.SetDefaultRouting(_config, item) == 0) + { + await RefreshRoutingItems(); + IsModified = true; + } + } + + private async Task RoutingAdvancedImportRules() + { + if (await ConfigHandler.InitRouting(_config, true) == 0) + { + await RefreshRoutingItems(); + IsModified = true; + } + } +} diff --git a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs new file mode 100644 index 00000000..e9ee033e --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs @@ -0,0 +1,555 @@ +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Text; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Splat; + +namespace ServiceLib.ViewModels; + +public class StatusBarViewModel : MyReactiveObject +{ + #region ObservableCollection + + public IObservableCollection RoutingItems { get; } = new ObservableCollectionExtended(); + + public IObservableCollection Servers { get; } = new ObservableCollectionExtended(); + + [Reactive] + public RoutingItem SelectedRouting { get; set; } + + [Reactive] + public ComboItem SelectedServer { get; set; } + + [Reactive] + public bool BlServers { get; set; } + + #endregion ObservableCollection + + public ReactiveCommand AddServerViaClipboardCmd { get; } + public ReactiveCommand AddServerViaScanCmd { get; } + public ReactiveCommand SubUpdateCmd { get; } + public ReactiveCommand SubUpdateViaProxyCmd { get; } + public ReactiveCommand CopyProxyCmdToClipboardCmd { get; } + public ReactiveCommand NotifyLeftClickCmd { get; } + public ReactiveCommand ShowWindowCmd { get; } + public ReactiveCommand HideWindowCmd { get; } + + #region System Proxy + + [Reactive] + public bool BlSystemProxyClear { get; set; } + + [Reactive] + public bool BlSystemProxySet { get; set; } + + [Reactive] + public bool BlSystemProxyNothing { get; set; } + + [Reactive] + public bool BlSystemProxyPac { get; set; } + + public ReactiveCommand SystemProxyClearCmd { get; } + public ReactiveCommand SystemProxySetCmd { get; } + public ReactiveCommand SystemProxyNothingCmd { get; } + public ReactiveCommand SystemProxyPacCmd { get; } + + [Reactive] + public bool BlRouting { get; set; } + + [Reactive] + public int SystemProxySelected { get; set; } + + [Reactive] + public bool BlSystemProxyPacVisible { get; set; } + + #endregion System Proxy + + #region UI + + [Reactive] + public string InboundDisplay { get; set; } + + [Reactive] + public string InboundLanDisplay { get; set; } + + [Reactive] + public string RunningServerDisplay { get; set; } + + [Reactive] + public string RunningServerToolTipText { get; set; } + + [Reactive] + public string RunningInfoDisplay { get; set; } + + [Reactive] + public string SpeedProxyDisplay { get; set; } + + [Reactive] + public string SpeedDirectDisplay { get; set; } + + [Reactive] + public bool EnableTun { get; set; } + + [Reactive] + public bool BlIsNonWindows { get; set; } + + #endregion UI + + public StatusBarViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + SelectedRouting = new(); + SelectedServer = new(); + RunningServerToolTipText = "-"; + BlSystemProxyPacVisible = Utils.IsWindows(); + BlIsNonWindows = Utils.IsNonWindows(); + + if (_config.TunModeItem.EnableTun && AllowEnableTun()) + { + EnableTun = true; + } + else + { + _config.TunModeItem.EnableTun = EnableTun = false; + } + + #region WhenAnyValue && ReactiveCommand + + this.WhenAnyValue( + x => x.SelectedRouting, + y => y != null && !y.Remarks.IsNullOrEmpty()) + .Subscribe(async c => await RoutingSelectedChangedAsync(c)); + + this.WhenAnyValue( + x => x.SelectedServer, + y => y != null && !y.Text.IsNullOrEmpty()) + .Subscribe(c => ServerSelectedChanged(c)); + + SystemProxySelected = (int)_config.SystemProxyItem.SysProxyType; + this.WhenAnyValue( + x => x.SystemProxySelected, + y => y >= 0) + .Subscribe(async c => await DoSystemProxySelected(c)); + + this.WhenAnyValue( + x => x.EnableTun, + y => y == true) + .Subscribe(async c => await DoEnableTun(c)); + + CopyProxyCmdToClipboardCmd = ReactiveCommand.CreateFromTask(async () => + { + await CopyProxyCmdToClipboard(); + }); + + NotifyLeftClickCmd = ReactiveCommand.CreateFromTask(async () => + { + Locator.Current.GetService()?.ShowHideWindow(null); + await Task.CompletedTask; + }); + ShowWindowCmd = ReactiveCommand.CreateFromTask(async () => + { + Locator.Current.GetService()?.ShowHideWindow(true); + await Task.CompletedTask; + }); + HideWindowCmd = ReactiveCommand.CreateFromTask(async () => + { + Locator.Current.GetService()?.ShowHideWindow(false); + await Task.CompletedTask; + }); + + AddServerViaClipboardCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerViaClipboard(); + }); + AddServerViaScanCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerViaScan(); + }); + SubUpdateCmd = ReactiveCommand.CreateFromTask(async () => + { + await UpdateSubscriptionProcess(false); + }); + SubUpdateViaProxyCmd = ReactiveCommand.CreateFromTask(async () => + { + await UpdateSubscriptionProcess(true); + }); + + //System proxy + SystemProxyClearCmd = ReactiveCommand.CreateFromTask(async () => + { + await SetListenerType(ESysProxyType.ForcedClear); + }); + SystemProxySetCmd = ReactiveCommand.CreateFromTask(async () => + { + await SetListenerType(ESysProxyType.ForcedChange); + }); + SystemProxyNothingCmd = ReactiveCommand.CreateFromTask(async () => + { + await SetListenerType(ESysProxyType.Unchanged); + }); + SystemProxyPacCmd = ReactiveCommand.CreateFromTask(async () => + { + await SetListenerType(ESysProxyType.Pac); + }); + + #endregion WhenAnyValue && ReactiveCommand + + #region AppEvents + + if (updateView != null) + { + InitUpdateView(updateView); + } + + AppEvents.DispatcherStatisticsRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async result => await UpdateStatistics(result)); + + #endregion AppEvents + + _ = Init(); + } + + private async Task Init() + { + await RefreshRoutingsMenu(); + await InboundDisplayStatus(); + await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, true); + } + + public void InitUpdateView(Func>? updateView) + { + _updateView = updateView; + if (_updateView != null) + { + AppEvents.ProfilesRefreshRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await RefreshServersBiz()); //.DisposeWith(_disposables); + } + } + + private async Task CopyProxyCmdToClipboard() + { + var cmd = Utils.IsWindows() ? "set" : "export"; + var address = $"{Global.Loopback}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}"; + + var sb = new StringBuilder(); + sb.AppendLine($"{cmd} http_proxy={Global.HttpProtocol}{address}"); + sb.AppendLine($"{cmd} https_proxy={Global.HttpProtocol}{address}"); + sb.AppendLine($"{cmd} all_proxy={Global.Socks5Protocol}{address}"); + sb.AppendLine(""); + sb.AppendLine($"{cmd} HTTP_PROXY={Global.HttpProtocol}{address}"); + sb.AppendLine($"{cmd} HTTPS_PROXY={Global.HttpProtocol}{address}"); + sb.AppendLine($"{cmd} ALL_PROXY={Global.Socks5Protocol}{address}"); + + await _updateView?.Invoke(EViewAction.SetClipboardData, sb.ToString()); + } + + private async Task AddServerViaClipboard() + { + var service = Locator.Current.GetService(); + if (service != null) + await service.AddServerViaClipboardAsync(null); + } + + private async Task AddServerViaScan() + { + var service = Locator.Current.GetService(); + if (service != null) + await service.AddServerViaScanAsync(); + } + + private async Task UpdateSubscriptionProcess(bool blProxy) + { + var service = Locator.Current.GetService(); + if (service != null) + await service.UpdateSubscriptionProcess("", blProxy); + } + + private async Task RefreshServersBiz() + { + await RefreshServersMenu(); + + //display running server + var running = await ConfigHandler.GetDefaultServer(_config); + if (running != null) + { + RunningServerDisplay = + RunningServerToolTipText = running.GetSummary(); + } + else + { + RunningServerDisplay = + RunningServerToolTipText = ResUI.CheckServerSettings; + } + } + + private async Task RefreshServersMenu() + { + var lstModel = await AppManager.Instance.ProfileItems(_config.SubIndexId, ""); + + Servers.Clear(); + if (lstModel.Count > _config.GuiItem.TrayMenuServersLimit) + { + BlServers = false; + return; + } + + BlServers = true; + for (int k = 0; k < lstModel.Count; k++) + { + ProfileItem it = lstModel[k]; + string name = it.GetSummary(); + + var item = new ComboItem() { ID = it.IndexId, Text = name }; + Servers.Add(item); + if (_config.IndexId == it.IndexId) + { + SelectedServer = item; + } + } + } + + private void ServerSelectedChanged(bool c) + { + if (!c) + { + return; + } + if (SelectedServer == null) + { + return; + } + if (SelectedServer.ID.IsNullOrEmpty()) + { + return; + } + Locator.Current.GetService()?.SetDefaultServer(SelectedServer.ID); + } + + public async Task TestServerAvailability() + { + var item = await ConfigHandler.GetDefaultServer(_config); + if (item == null) + { + return; + } + + await TestServerAvailabilitySub(ResUI.Speedtesting); + + var msg = await Task.Run(ConnectionHandler.RunAvailabilityCheck); + + NoticeManager.Instance.SendMessageEx(msg); + await TestServerAvailabilitySub(msg); + } + + private async Task TestServerAvailabilitySub(string msg) + { + RxApp.MainThreadScheduler.Schedule(msg, (scheduler, msg) => + { + _ = TestServerAvailabilityResult(msg); + return Disposable.Empty; + }); + } + + public async Task TestServerAvailabilityResult(string msg) + { + RunningInfoDisplay = msg; + } + + #region System proxy and Routings + + public async Task SetListenerType(ESysProxyType type) + { + if (_config.SystemProxyItem.SysProxyType == type) + { + return; + } + _config.SystemProxyItem.SysProxyType = type; + await ChangeSystemProxyAsync(type, true); + NoticeManager.Instance.SendMessageEx($"{ResUI.TipChangeSystemProxy} - {_config.SystemProxyItem.SysProxyType.ToString()}"); + + SystemProxySelected = (int)_config.SystemProxyItem.SysProxyType; + await ConfigHandler.SaveConfig(_config); + } + + public async Task ChangeSystemProxyAsync(ESysProxyType type, bool blChange) + { + await SysProxyHandler.UpdateSysProxy(_config, false); + + BlSystemProxyClear = (type == ESysProxyType.ForcedClear); + BlSystemProxySet = (type == ESysProxyType.ForcedChange); + BlSystemProxyNothing = (type == ESysProxyType.Unchanged); + BlSystemProxyPac = (type == ESysProxyType.Pac); + + if (blChange) + { + _updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null); + } + } + + public async Task RefreshRoutingsMenu() + { + RoutingItems.Clear(); + + BlRouting = true; + var routings = await AppManager.Instance.RoutingItems(); + foreach (var item in routings) + { + RoutingItems.Add(item); + if (item.IsActive) + { + SelectedRouting = item; + } + } + } + + private async Task RoutingSelectedChangedAsync(bool c) + { + if (!c) + { + return; + } + + if (SelectedRouting == null) + { + return; + } + + var item = await AppManager.Instance.GetRoutingItem(SelectedRouting?.Id); + if (item is null) + { + return; + } + + if (await ConfigHandler.SetDefaultRouting(_config, item) == 0) + { + NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting); + Locator.Current.GetService()?.Reload(); + _updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null); + } + } + + private async Task DoSystemProxySelected(bool c) + { + if (!c) + { + return; + } + if (_config.SystemProxyItem.SysProxyType == (ESysProxyType)SystemProxySelected) + { + return; + } + await SetListenerType((ESysProxyType)SystemProxySelected); + } + + private async Task DoEnableTun(bool c) + { + if (_config.TunModeItem.EnableTun == EnableTun) + { + return; + } + + _config.TunModeItem.EnableTun = EnableTun; + + if (EnableTun && AllowEnableTun() == false) + { + // When running as a non-administrator, reboot to administrator mode + if (Utils.IsWindows()) + { + _config.TunModeItem.EnableTun = false; + Locator.Current.GetService()?.RebootAsAdmin(); + return; + } + else + { + bool? passwordResult = await _updateView?.Invoke(EViewAction.PasswordInput, null); + if (passwordResult == false) + { + _config.TunModeItem.EnableTun = false; + return; + } + } + } + await ConfigHandler.SaveConfig(_config); + Locator.Current.GetService()?.Reload(); + } + + private bool AllowEnableTun() + { + if (Utils.IsWindows()) + { + return Utils.IsAdministrator(); + } + else if (Utils.IsLinux()) + { + return AppManager.Instance.LinuxSudoPwd.IsNotEmpty(); + } + else if (Utils.IsOSX()) + { + return AppManager.Instance.LinuxSudoPwd.IsNotEmpty(); + } + return false; + } + + #endregion System proxy and Routings + + #region UI + + public async Task InboundDisplayStatus() + { + StringBuilder sb = new(); + sb.Append($"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}"); + if (_config.Inbound.First().SecondLocalPortEnabled) + { + sb.Append($",{AppManager.Instance.GetLocalPort(EInboundProtocol.socks2)}"); + } + sb.Append(']'); + InboundDisplay = $"{ResUI.LabLocal}:{sb}"; + + if (_config.Inbound.First().AllowLANConn) + { + var lan = _config.Inbound.First().NewPort4LAN + ? $"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks3)}]" + : $"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}]"; + InboundLanDisplay = $"{ResUI.LabLAN}:{lan}"; + } + else + { + InboundLanDisplay = $"{ResUI.LabLAN}:{Global.None}"; + } + await Task.CompletedTask; + } + + public async Task UpdateStatistics(ServerSpeedItem update) + { + if (!_config.GuiItem.DisplayRealTimeSpeed) + { + return; + } + + try + { + if (_config.IsRunningCore(ECoreType.sing_box)) + { + SpeedProxyDisplay = string.Format(ResUI.SpeedDisplayText, EInboundProtocol.mixed, Utils.HumanFy(update.ProxyUp), Utils.HumanFy(update.ProxyDown)); + SpeedDirectDisplay = string.Empty; + } + else + { + SpeedProxyDisplay = string.Format(ResUI.SpeedDisplayText, Global.ProxyTag, Utils.HumanFy(update.ProxyUp), Utils.HumanFy(update.ProxyDown)); + SpeedDirectDisplay = string.Format(ResUI.SpeedDisplayText, Global.DirectTag, Utils.HumanFy(update.DirectUp), Utils.HumanFy(update.DirectDown)); + } + } + catch + { + } + } + + #endregion UI +} diff --git a/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs b/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs new file mode 100644 index 00000000..bfbfbbe7 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs @@ -0,0 +1,63 @@ +using System.Reactive; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class SubEditViewModel : MyReactiveObject +{ + [Reactive] + public SubItem SelectedSource { get; set; } + + public ReactiveCommand SaveCmd { get; } + + public SubEditViewModel(SubItem subItem, Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + SaveCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveSubAsync(); + }); + + SelectedSource = subItem.Id.IsNullOrEmpty() ? subItem : JsonUtils.DeepCopy(subItem); + } + + private async Task SaveSubAsync() + { + var remarks = SelectedSource.Remarks; + if (remarks.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); + return; + } + + var url = SelectedSource.Url; + if (url.IsNotEmpty()) + { + var uri = Utils.TryUri(url); + if (uri == null) + { + NoticeManager.Instance.Enqueue(ResUI.InvalidUrlTip); + return; + } + //Do not allow http protocol + if (url.StartsWith(Global.HttpProtocol) && !Utils.IsPrivateNetwork(uri.IdnHost)) + { + NoticeManager.Instance.Enqueue(ResUI.InsecureUrlProtocol); + //return; + } + } + + if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0) + { + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + _updateView?.Invoke(EViewAction.CloseWindow, null); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } +} diff --git a/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs new file mode 100644 index 00000000..88f33619 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs @@ -0,0 +1,103 @@ +using System.Reactive; +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class SubSettingViewModel : MyReactiveObject +{ + public IObservableCollection SubItems { get; } = new ObservableCollectionExtended(); + + [Reactive] + public SubItem SelectedSource { get; set; } + + public IList SelectedSources { get; set; } + + public ReactiveCommand SubAddCmd { get; } + public ReactiveCommand SubDeleteCmd { get; } + public ReactiveCommand SubEditCmd { get; } + public ReactiveCommand SubShareCmd { get; } + public bool IsModified { get; set; } + + public SubSettingViewModel(Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + var canEditRemove = this.WhenAnyValue( + x => x.SelectedSource, + selectedSource => selectedSource != null && !selectedSource.Id.IsNullOrEmpty()); + + SubAddCmd = ReactiveCommand.CreateFromTask(async () => + { + await EditSubAsync(true); + }); + SubDeleteCmd = ReactiveCommand.CreateFromTask(async () => + { + await DeleteSubAsync(); + }, canEditRemove); + SubEditCmd = ReactiveCommand.CreateFromTask(async () => + { + await EditSubAsync(false); + }, canEditRemove); + SubShareCmd = ReactiveCommand.CreateFromTask(async () => + { + await _updateView?.Invoke(EViewAction.ShareSub, SelectedSource?.Url); + }, canEditRemove); + + _ = Init(); + } + + private async Task Init() + { + SelectedSource = new(); + + await RefreshSubItems(); + } + + public async Task RefreshSubItems() + { + SubItems.Clear(); + SubItems.AddRange(await AppManager.Instance.SubItems()); + } + + public async Task EditSubAsync(bool blNew) + { + SubItem item; + if (blNew) + { + item = new(); + } + else + { + item = await AppManager.Instance.GetSubItem(SelectedSource?.Id); + if (item is null) + { + return; + } + } + if (await _updateView?.Invoke(EViewAction.SubEditWindow, item) == true) + { + await RefreshSubItems(); + IsModified = true; + } + } + + private async Task DeleteSubAsync() + { + if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) + { + return; + } + + foreach (var it in SelectedSources ?? [SelectedSource]) + { + await ConfigHandler.DeleteSubItem(_config, it.Id); + } + await RefreshSubItems(); + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + IsModified = true; + } +} diff --git a/v2rayN/v2rayN.Desktop/App.axaml b/v2rayN/v2rayN.Desktop/App.axaml new file mode 100644 index 00000000..8b61125f --- /dev/null +++ b/v2rayN/v2rayN.Desktop/App.axaml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/App.axaml.cs b/v2rayN/v2rayN.Desktop/App.axaml.cs new file mode 100644 index 00000000..5303eb6c --- /dev/null +++ b/v2rayN/v2rayN.Desktop/App.axaml.cs @@ -0,0 +1,80 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using ServiceLib.Manager; +using Splat; +using v2rayN.Desktop.Common; +using v2rayN.Desktop.Views; + +namespace v2rayN.Desktop; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; + + var ViewModel = new StatusBarViewModel(null); + Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(StatusBarViewModel)); + DataContext = ViewModel; + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + AppManager.Instance.InitComponents(); + + desktop.Exit += OnExit; + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } + + private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + if (e.ExceptionObject != null) + { + Logging.SaveLog("CurrentDomain_UnhandledException", (Exception)e.ExceptionObject); + } + } + + private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + { + Logging.SaveLog("TaskScheduler_UnobservedTaskException", e.Exception); + } + + private void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs e) + { + } + + private async void MenuAddServerViaClipboardClick(object? sender, EventArgs e) + { + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + if (desktop.MainWindow != null) + { + var clipboardData = await AvaUtils.GetClipboardData(desktop.MainWindow); + if (clipboardData.IsNullOrEmpty()) + { + return; + } + var service = Locator.Current.GetService(); + if (service != null) + { + _ = service.AddServerViaClipboardAsync(clipboardData); + } + } + } + } + + private async void MenuExit_Click(object? sender, EventArgs e) + { + await AppManager.Instance.AppExitAsync(false); + AppManager.Instance.Shutdown(true); + } +} diff --git a/v2rayN/v2rayN.Desktop/Assets/Fonts/NotoSansSC-Regular.ttf b/v2rayN/v2rayN.Desktop/Assets/Fonts/NotoSansSC-Regular.ttf new file mode 100644 index 00000000..7056f5e9 Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/Fonts/NotoSansSC-Regular.ttf differ diff --git a/v2rayN/v2rayN.Desktop/Assets/GlobalResources.axaml b/v2rayN/v2rayN.Desktop/Assets/GlobalResources.axaml new file mode 100644 index 00000000..f58db043 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Assets/GlobalResources.axaml @@ -0,0 +1,21 @@ + + M512 0a512 512 0 1 0 512 512A512 512 0 0 0 512 0z m47.402667 580.906667l-50.090667 50.816q-25.045333 24.448-45.098667 44.8c-13.397333 13.610667-24.405333 24.704-33.194666 33.493333s-13.994667 13.781333-15.701334 15.018667c-4.266667 3.797333-9.002667 7.722667-14.421333 11.904a77.397333 77.397333 0 0 1-17.066667 9.984 255.232 255.232 0 0 1-25.6 10.581333 832.426667 832.426667 0 0 1-70.528 22.485333c-11.093333 2.901333-19.285333 4.821333-24.704 5.589334-11.306667 1.28-18.816-0.384-22.485333-4.992a29.397333 29.397333 0 0 1-3.114667-23.210667 198.016 198.016 0 0 1 6.016-25.6c3.114667-11.093333 6.485333-22.485333 9.984-34.389333s6.997333-22.912 10.282667-32.896a164.821333 164.821333 0 0 1 8.106667-20.693334 96.64 96.64 0 0 1 8.533333-16 101.504 101.504 0 0 1 13.482667-16c2.517333-2.517333 8.533333-8.533333 17.493333-17.792s20.693333-20.992 34.389333-34.816l231.253334-231.253333 117.12 118.016-134.741334 134.741333z m224.938666-229.333334a51.2 51.2 0 0 1-9.984 15.701334q-7.509333 7.509333-13.482666 13.184c-4.010667 3.797333-7.381333 7.082667-10.282667 9.984s-6.485333 6.314667-9.386667 8.789333l-117.290666-117.162667c4.992-4.608 11.008-10.112 17.92-16.597333s12.8-11.605333 17.066666-15.317333a54.613333 54.613333 0 0 1 18.218667-9.685334 56.917333 56.917333 0 0 1 18.517333-2.517333 71.936 71.936 0 0 1 17.493334 2.816 94.464 94.464 0 0 1 14.72 5.589333 113.536 113.536 0 0 1 29.098666 24.106667 138.666667 138.666667 0 0 1 24.704 36.010667 66.389333 66.389333 0 0 1 4.266667 13.184 79.573333 79.573333 0 0 1 1.621333 15.701333 44.330667 44.330667 0 0 1-3.114666 16.213333z m0 0 + M512 0C229.376 0 0 229.376 0 512s229.376 512 512 512 512-229.376 512-512S794.624 0 512 0z m238.08 570.88h-179.2v179.2c0 32.768-26.112 58.88-58.88 58.88s-58.88-26.112-58.88-58.88v-179.2h-179.2c-32.768 0-58.88-26.112-58.88-58.88s26.112-58.88 58.88-58.88h179.2v-179.2c0-32.768 26.112-58.88 58.88-58.88s58.88 26.112 58.88 58.88v179.2h179.2c32.768 0 58.88 26.112 58.88 58.88s-26.112 58.88-58.88 58.88z + M512 1024C229.248 1024 0 794.752 0 512S229.248 0 512 0s512 229.248 512 512-229.248 512-512 512zM216.576 488.789333h-23.168v46.421334h23.168v56.448c0 7.296 5.973333 13.226667 13.269333 13.226666h42.965334c7.296 0 13.226667-5.930667 13.226666-13.226666v-159.317334a13.269333 13.269333 0 0 0-13.226666-13.226666H229.845333a13.269333 13.269333 0 0 0-13.226666 13.226666v56.448z m415.104 0.426667H403.712V366.037333a13.269333 13.269333 0 0 0-13.269333-13.269333H325.802667a13.269333 13.269333 0 0 0-13.226667 13.226667v292.053333c0 7.338667 5.888 13.269333 13.226667 13.269333h64.64c7.338667 0 13.269333-5.930667 13.269333-13.226666v-123.306667h227.968v123.306667c0 7.296 5.930667 13.226667 13.226667 13.226666h64.64c7.338667 0 13.269333-5.930667 13.269333-13.226666V365.952a13.269333 13.269333 0 0 0-13.226667-13.269333h-64.64a13.269333 13.269333 0 0 0-13.269333 13.226666v123.306667z m187.093333-0.426667v-56.448a13.269333 13.269333 0 0 0-13.226666-13.226666h-42.965334a13.269333 13.269333 0 0 0-13.269333 13.226666v159.317334c0 7.296 5.973333 13.226667 13.269333 13.226666h42.922667c7.338667 0 13.269333-5.930667 13.269333-13.226666v-56.448h23.168v-46.421334h-23.168z + M511.9,276.3c43.8,0 79.2,-35.5 79.2,-79.2 0,-43.8 -35.5,-79.2 -79.2,-79.2 -43.8,0 -79.2,35.5 -79.2,79.2 0,43.8 35.5,79.2 79.2,79.2zM511.9,434.8c-43.8,0 -79.2,35.5 -79.2,79.2 0,43.8 35.5,79.2 79.2,79.2 43.8,0 79.2,-35.5 79.2,-79.2 0,-43.8 -35.5,-79.2 -79.2,-79.2zM511.9,751.8c-43.8,0 -79.2,35.4 -79.2,79.2 0,43.8 35.5,79.2 79.2,79.2 43.8,0 79.2,-35.5 79.2,-79.2 0,-43.8 -35.5,-79.2 -79.2,-79.2z + M809.984 169.984l0 86.016-596.010667 0 0-86.016 148.010667 0 43.989333-41.984 212.010667 0 43.989333 41.984 148.010667 0zM256 809.984l0-512 512 0 0 512q0 34.005333-25.984 59.989333t-59.989333 25.984l-340.010667 0q-34.005333 0-59.989333-25.984t-25.984-59.989333z + M704 896v80c0 26.51-21.49 48-48 48H112c-26.51 0-48-21.49-48-48V240c0-26.51 21.49-48 48-48h144v592c0 61.758 50.242 112 112 112h336z m0-688V0H368c-26.51 0-48 21.49-48 48v736c0 26.51 21.49 48 48 48h544c26.51 0 48-21.49 48-48V256H752c-26.4 0-48-21.6-48-48z m241.942-62.058L814.058 14.058A48 48 0 0 0 780.118 0H768v192h192v-12.118a48 48 0 0 0-14.058-33.94z + M849.652671 679.144788l111.007233-174.965917-50.615794 0C905.498584 274.107915 717.720873 88.965218 486.575446 88.965218c-233.998405 0-423.716304 189.698456-423.716304 423.707095 0 233.998405 189.716876 423.715281 423.716304 423.715281 113.936959 0 217.278605-45.079708 293.440216-118.235868l-62.46568-108.306728c-55.750745 65.205071-138.455375 106.709347-230.974535 106.709347-167.843706 0-303.882032-136.039349-303.882032-303.883055S318.732763 208.788234 486.575446 208.788234c164.951843 0 298.899554 131.522476 303.44508 295.389614l-51.357691 0L849.652671 679.144788z + M273.28 899.328c-6.4 6.4-16 9.6-25.6 9.6-6.4 0-12.8-3.2-18.56-6.4-102.4-85.76-162.56-209.92-162.56-343.68 0-245.12 200.32-445.44 445.44-445.44s445.44 200.32 445.44 445.44c0 133.76-56.96 257.92-162.56 343.68-12.16 12.8-34.56 9.6-44.16-3.2-12.8-12.8-9.6-35.2 3.2-44.8a377.152 377.152 0 0 0 136.96-292.48c0-209.92-172.16-382.08-382.08-382.08-206.72-3.2-378.88 168.96-378.88 378.88 0 114.56 51.2 222.72 140.16 295.68 12.8 12.8 16 32 3.2 44.8z m394.88-540.8c12.8-12.8 31.36-12.8 44.16 0 12.8 12.8 12.8 32 0 44.8l-138.88 138.88c1.28 5.12 2.56 10.88 2.56 16.64 0 35.2-28.8 64-64 64-5.76 0-11.52-1.28-16.64-2.56l-24.32 24.32c-6.4 6.4-12.8 9.6-22.4 9.6-9.6 0-16-3.2-22.4-9.6-12.8-12.8-12.8-31.36 0-44.16l24.32-24.96a69.76 69.76 0 0 1-1.92-16.64c0-35.2 28.16-63.36 63.36-63.36 5.76 0 11.52 0.64 16.64 1.92z + + 32 + 32 + 1000 + + 2 + 4,0 + 4 + 8,0 + 0,8 + 8 + diff --git a/v2rayN/v2rayN.Desktop/Assets/GlobalStyles.axaml b/v2rayN/v2rayN.Desktop/Assets/GlobalStyles.axaml new file mode 100644 index 00000000..6a2cfacd --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Assets/GlobalStyles.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Assets/NotifyIcon1.ico b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon1.ico new file mode 100644 index 00000000..a978e0a8 Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon1.ico differ diff --git a/v2rayN/v2rayN.Desktop/Assets/NotifyIcon2.ico b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon2.ico new file mode 100644 index 00000000..b625aa8e Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon2.ico differ diff --git a/v2rayN/v2rayN.Desktop/Assets/NotifyIcon3.ico b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon3.ico new file mode 100644 index 00000000..6b6db8e6 Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon3.ico differ diff --git a/v2rayN/v2rayN.Desktop/Assets/NotifyIcon4.ico b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon4.ico new file mode 100644 index 00000000..05a3d011 Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon4.ico differ diff --git a/v2rayN/v2rayN.Desktop/Assets/v2rayN.ico b/v2rayN/v2rayN.Desktop/Assets/v2rayN.ico new file mode 100644 index 00000000..a978e0a8 Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/v2rayN.ico differ diff --git a/v2rayN/v2rayN.Desktop/Base/WindowBase.cs b/v2rayN/v2rayN.Desktop/Base/WindowBase.cs new file mode 100644 index 00000000..c2c9a0bc --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Base/WindowBase.cs @@ -0,0 +1,53 @@ +using Avalonia; +using Avalonia.Interactivity; +using Avalonia.ReactiveUI; +using ServiceLib.Manager; + +namespace v2rayN.Desktop.Base; + +public class WindowBase : ReactiveWindow where TViewModel : class +{ + public WindowBase() + { + Loaded += OnLoaded; + } + + private void ReactiveWindowBase_Closed(object? sender, EventArgs e) + { + throw new NotImplementedException(); + } + + protected virtual void OnLoaded(object? sender, RoutedEventArgs e) + { + try + { + var sizeItem = ConfigHandler.GetWindowSizeItem(AppManager.Instance.Config, GetType().Name); + if (sizeItem == null) + { + return; + } + + Width = sizeItem.Width; + Height = sizeItem.Height; + + var workingArea = (Screens.ScreenFromWindow(this) ?? Screens.Primary).WorkingArea; + var scaling = (Utils.IsOSX() ? null : VisualRoot?.RenderScaling) ?? 1.0; + + var x = workingArea.X + ((workingArea.Width - (Width * scaling)) / 2); + var y = workingArea.Y + ((workingArea.Height - (Height * scaling)) / 2); + + Position = new PixelPoint((int)x, (int)y); + } + catch { } + } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + try + { + ConfigHandler.SaveWindowSizeItem(AppManager.Instance.Config, GetType().Name, Width, Height); + } + catch { } + } +} diff --git a/v2rayN/v2rayN.Desktop/Common/AppBuilderExtension.cs b/v2rayN/v2rayN.Desktop/Common/AppBuilderExtension.cs new file mode 100644 index 00000000..3c5169cc --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Common/AppBuilderExtension.cs @@ -0,0 +1,17 @@ +using Avalonia; +using Avalonia.Media; + +namespace v2rayN.Desktop.Common; + +public static class AppBuilderExtension +{ + public static AppBuilder WithFontByDefault(this AppBuilder appBuilder) + { + var uri = Path.Combine(Global.AvaAssets, "Fonts#Noto Sans SC"); + return appBuilder.With(new FontManagerOptions() + { + //DefaultFamilyName = uri, + FontFallbacks = new[] { new FontFallback { FontFamily = new FontFamily(uri) } } + }); + } +} diff --git a/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs b/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs new file mode 100644 index 00000000..87c974d4 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs @@ -0,0 +1,58 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace v2rayN.Desktop.Common; + +internal class AvaUtils +{ + public static async Task GetClipboardData(Window owner) + { + try + { + var clipboard = TopLevel.GetTopLevel(owner)?.Clipboard; + if (clipboard == null) + { + return null; + } + + return await clipboard.GetTextAsync(); + } + catch + { + return null; + } + } + + public static async Task SetClipboardData(Visual? visual, string strData) + { + try + { + var clipboard = TopLevel.GetTopLevel(visual)?.Clipboard; + if (clipboard == null) + return; + var dataObject = new DataObject(); + dataObject.Set(DataFormats.Text, strData); + await clipboard.SetDataObjectAsync(dataObject); + } + catch + { + } + } + + public static WindowIcon GetAppIcon(ESysProxyType sysProxyType) + { + var index = (int)sysProxyType + 1; + var fileName = Utils.GetPath($"NotifyIcon{index}.ico"); + if (File.Exists(fileName)) + { + return new(fileName); + } + + var uri = new Uri(Path.Combine(Global.AvaAssets, $"NotifyIcon{index}.ico")); + using var bitmap = new Bitmap(AssetLoader.Open(uri)); + return new(bitmap); + } +} diff --git a/v2rayN/v2rayN.Desktop/Common/UI.cs b/v2rayN/v2rayN.Desktop/Common/UI.cs new file mode 100644 index 00000000..e9ddc36f --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Common/UI.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using Avalonia.Platform.Storage; +using MsBox.Avalonia; +using MsBox.Avalonia.Enums; + +namespace v2rayN.Desktop.Common; + +internal class UI +{ + private static readonly string caption = Global.AppName; + + public static async Task ShowYesNo(Window owner, string msg) + { + var box = MessageBoxManager.GetMessageBoxStandard(caption, msg, ButtonEnum.YesNo); + return await box.ShowWindowDialogAsync(owner); + } + + public static async Task OpenFileDialog(Window owner, FilePickerFileType? filter) + { + var sp = GetStorageProvider(owner); + if (sp is null) + { + return null; + } + + // Start async operation to open the dialog. + var files = await sp.OpenFilePickerAsync(new FilePickerOpenOptions + { + AllowMultiple = false, + FileTypeFilter = filter is null ? [FilePickerFileTypes.All, FilePickerFileTypes.ImagePng] : [filter] + }); + + return files.FirstOrDefault()?.TryGetLocalPath(); + } + + public static async Task SaveFileDialog(Window owner, string filter) + { + var sp = GetStorageProvider(owner); + if (sp is null) + { + return null; + } + + // Start async operation to open the dialog. + var files = await sp.SaveFilePickerAsync(new FilePickerSaveOptions + { + }); + + return files?.TryGetLocalPath(); + } + + private static IStorageProvider? GetStorageProvider(Window owner) + { + var topLevel = TopLevel.GetTopLevel(owner); + return topLevel?.StorageProvider; + } +} diff --git a/v2rayN/v2rayN.Desktop/Controls/AutoCompleteBox.axaml b/v2rayN/v2rayN.Desktop/Controls/AutoCompleteBox.axaml new file mode 100644 index 00000000..97fec908 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Controls/AutoCompleteBox.axaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Controls/AutoCompleteBox.cs b/v2rayN/v2rayN.Desktop/Controls/AutoCompleteBox.cs new file mode 100644 index 00000000..82e04d4b --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Controls/AutoCompleteBox.cs @@ -0,0 +1,40 @@ +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace v2rayN.Desktop.Controls; + +public class AutoCompleteBox : Avalonia.Controls.AutoCompleteBox +{ + static AutoCompleteBox() + { + MinimumPrefixLengthProperty.OverrideDefaultValue(0); + } + + public AutoCompleteBox() + { + AddHandler(PointerPressedEvent, OnBoxPointerPressed, RoutingStrategies.Tunnel); + } + + private void OnBoxPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (Equals(sender, this) && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + { + SetCurrentValue(IsDropDownOpenProperty, true); + } + } + + protected override void OnGotFocus(GotFocusEventArgs e) + { + base.OnGotFocus(e); + if (IsDropDownOpen) + { + return; + } + SetCurrentValue(IsDropDownOpenProperty, true); + } + + public void Clear() + { + SetCurrentValue(SelectedItemProperty, null); + } +} diff --git a/v2rayN/v2rayN.Desktop/Converters/DelayColorConverter.cs b/v2rayN/v2rayN.Desktop/Converters/DelayColorConverter.cs new file mode 100644 index 00000000..05364578 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Converters/DelayColorConverter.cs @@ -0,0 +1,25 @@ +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace v2rayN.Desktop.Converters; + +public class DelayColorConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + _ = int.TryParse(value?.ToString(), out var delay); + + return delay switch + { + <= 0 => new SolidColorBrush(Colors.Red), + <= 500 => new SolidColorBrush(Colors.Green), + _ => new SolidColorBrush(Colors.IndianRed) + }; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return null; + } +} diff --git a/v2rayN/v2rayN.Desktop/FodyWeavers.xml b/v2rayN/v2rayN.Desktop/FodyWeavers.xml new file mode 100644 index 00000000..63fc1484 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/v2rayN/v2rayN.Desktop/GlobalUsings.cs b/v2rayN/v2rayN.Desktop/GlobalUsings.cs new file mode 100644 index 00000000..bc789ab0 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/GlobalUsings.cs @@ -0,0 +1,8 @@ +global using ServiceLib; +global using ServiceLib.Base; +global using ServiceLib.Common; +global using ServiceLib.Enums; +global using ServiceLib.Handler; +global using ServiceLib.Models; +global using ServiceLib.Resx; +global using ServiceLib.ViewModels; \ No newline at end of file diff --git a/v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs b/v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs new file mode 100644 index 00000000..5ce6ff60 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs @@ -0,0 +1,90 @@ +using System.Reactive.Linq; +using Avalonia.Input; +using Avalonia.ReactiveUI; +using Avalonia.Win32.Input; +using GlobalHotKeys; + +namespace v2rayN.Desktop.Manager; + +public sealed class HotkeyManager +{ + private static readonly Lazy _instance = new(() => new()); + public static HotkeyManager Instance = _instance.Value; + private readonly Dictionary _hotkeyTriggerDic = new(); + private HotKeyManager? _hotKeyManager; + + private Config? _config; + + private event Action? _updateFunc; + + public bool IsPause { get; set; } = false; + + public void Init(Config config, Action updateFunc) + { + _config = config; + _updateFunc = updateFunc; + + Register(); + } + + public void Dispose() + { + _hotKeyManager?.Dispose(); + } + + private void Register() + { + if (_config.GlobalHotkeys.Any(t => t.KeyCode > 0) == false) + { + return; + } + _hotKeyManager ??= new GlobalHotKeys.HotKeyManager(); + _hotkeyTriggerDic.Clear(); + + foreach (var item in _config.GlobalHotkeys) + { + if (item.KeyCode is null or 0) + { + continue; + } + + var vKey = KeyInterop.VirtualKeyFromKey((Key)item.KeyCode); + var modifiers = Modifiers.None; + if (item.Control) + { + modifiers |= Modifiers.Control; + } + if (item.Shift) + { + modifiers |= Modifiers.Shift; + } + if (item.Alt) + { + modifiers |= Modifiers.Alt; + } + + var result = _hotKeyManager?.Register((VirtualKeyCode)vKey, modifiers); + if (result?.IsSuccessful == true) + { + _hotkeyTriggerDic.Add(result.Id, item.EGlobalHotkey); + } + } + + _hotKeyManager?.HotKeyPressed + .ObserveOn(AvaloniaScheduler.Instance) + .Subscribe(OnNext); + } + + private void OnNext(HotKey key) + { + if (_updateFunc == null || IsPause) + { + return; + } + + if (_hotkeyTriggerDic.TryGetValue(key.Id, out var value)) + { + _updateFunc?.Invoke(value); + } + } +} diff --git a/v2rayN/v2rayN.Desktop/Program.cs b/v2rayN/v2rayN.Desktop/Program.cs new file mode 100644 index 00000000..58ddb9f9 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Program.cs @@ -0,0 +1,68 @@ +using Avalonia; +using Avalonia.ReactiveUI; +using ServiceLib.Manager; +using v2rayN.Desktop.Common; + +namespace v2rayN.Desktop; + +internal class Program +{ + public static EventWaitHandle ProgramStarted; + + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) + { + if (OnStartup(args) == false) + { + Environment.Exit(0); + return; + } + + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } + + private static bool OnStartup(string[]? Args) + { + if (Utils.IsWindows()) + { + var exePathKey = Utils.GetMd5(Utils.GetExePath()); + var rebootas = (Args ?? []).Any(t => t == Global.RebootAs); + ProgramStarted = new EventWaitHandle(false, EventResetMode.AutoReset, exePathKey, out var bCreatedNew); + if (!rebootas && !bCreatedNew) + { + ProgramStarted.Set(); + return false; + } + } + else + { + _ = new Mutex(true, "v2rayN", out var bOnlyOneInstance); + if (!bOnlyOneInstance) + { + return false; + } + } + + if (!AppManager.Instance.InitApp()) + { + return false; + } + return true; + } + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + { + return AppBuilder.Configure() + .UsePlatformDetect() + //.WithInterFont() + .WithFontByDefault() + .LogToTrace() + .UseReactiveUI() + .With(new MacOSPlatformOptions { ShowInDock = AppManager.Instance.Config.UiItem.MacOSShowInDock }); + } +} diff --git a/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs b/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs new file mode 100644 index 00000000..21899bfc --- /dev/null +++ b/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs @@ -0,0 +1,171 @@ +using System.Reactive.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Notifications; +using Avalonia.Controls.Primitives; +using Avalonia.Media; +using Avalonia.Styling; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Semi.Avalonia; +using ServiceLib.Manager; + +namespace v2rayN.Desktop.ViewModels; + +public class ThemeSettingViewModel : MyReactiveObject +{ + [Reactive] public string CurrentTheme { get; set; } + + [Reactive] public int CurrentFontSize { get; set; } + + [Reactive] public string CurrentLanguage { get; set; } + + public ThemeSettingViewModel() + { + _config = AppManager.Instance.Config; + + BindingUI(); + RestoreUI(); + } + + private void RestoreUI() + { + ModifyTheme(); + ModifyFontFamily(); + ModifyFontSize(); + } + + private void BindingUI() + { + CurrentTheme = _config.UiItem.CurrentTheme; + CurrentFontSize = _config.UiItem.CurrentFontSize; + CurrentLanguage = _config.UiItem.CurrentLanguage; + + this.WhenAnyValue(x => x.CurrentTheme) + .Subscribe(c => + { + if (_config.UiItem.CurrentTheme != CurrentTheme) + { + _config.UiItem.CurrentTheme = CurrentTheme; + ModifyTheme(); + ConfigHandler.SaveConfig(_config); + } + }); + + this.WhenAnyValue( + x => x.CurrentFontSize, + y => y > 0) + .Subscribe(c => + { + if (_config.UiItem.CurrentFontSize != CurrentFontSize && CurrentFontSize >= Global.MinFontSize) + { + _config.UiItem.CurrentFontSize = CurrentFontSize; + ModifyFontSize(); + ConfigHandler.SaveConfig(_config); + } + }); + + this.WhenAnyValue( + x => x.CurrentLanguage, + y => y != null && !y.IsNullOrEmpty()) + .Subscribe(c => + { + if (CurrentLanguage.IsNotEmpty() && _config.UiItem.CurrentLanguage != CurrentLanguage) + { + _config.UiItem.CurrentLanguage = CurrentLanguage; + Thread.CurrentThread.CurrentUICulture = new(CurrentLanguage); + ConfigHandler.SaveConfig(_config); + NoticeManager.Instance.Enqueue(ResUI.NeedRebootTips); + } + }); + } + + private void ModifyTheme() + { + var app = Application.Current; + if (app is not null) + { + app.RequestedThemeVariant = CurrentTheme switch + { + nameof(ETheme.Dark) => ThemeVariant.Dark, + nameof(ETheme.Light) => ThemeVariant.Light, + nameof(ETheme.Aquatic) => SemiTheme.Aquatic, + nameof(ETheme.Desert) => SemiTheme.Desert, + nameof(ETheme.Dusk) => SemiTheme.Dusk, + nameof(ETheme.NightSky) => SemiTheme.NightSky, + _ => ThemeVariant.Default, + }; + } + } + + private void ModifyFontSize() + { + double size = CurrentFontSize; + if (size < Global.MinFontSize) + return; + + Style style = new(x => Selectors.Or( + x.OfType + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs new file mode 100644 index 00000000..6880e23d --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs @@ -0,0 +1,353 @@ +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Interactivity; +using ReactiveUI; +using ServiceLib.Manager; +using v2rayN.Desktop.Base; + +namespace v2rayN.Desktop.Views; + +public partial class AddServerWindow : WindowBase +{ + public AddServerWindow() + { + InitializeComponent(); + } + + public AddServerWindow(ProfileItem profileItem) + { + InitializeComponent(); + + this.Loaded += Window_Loaded; + btnCancel.Click += (s, e) => this.Close(); + cmbNetwork.SelectionChanged += CmbNetwork_SelectionChanged; + cmbStreamSecurity.SelectionChanged += CmbStreamSecurity_SelectionChanged; + btnGUID.Click += btnGUID_Click; + btnGUID5.Click += btnGUID_Click; + + ViewModel = new AddServerViewModel(profileItem, UpdateViewHandler); + + cmbCoreType.ItemsSource = Global.CoreTypes.AppendEmpty(); + cmbNetwork.ItemsSource = Global.Networks; + cmbFingerprint.ItemsSource = Global.Fingerprints; + cmbFingerprint2.ItemsSource = Global.Fingerprints; + cmbAllowInsecure.ItemsSource = Global.AllowInsecure; + cmbAlpn.ItemsSource = Global.Alpns; + + var lstStreamSecurity = new List(); + lstStreamSecurity.Add(string.Empty); + lstStreamSecurity.Add(Global.StreamSecurity); + + switch (profileItem.ConfigType) + { + case EConfigType.VMess: + gridVMess.IsVisible = true; + cmbSecurity.ItemsSource = Global.VmessSecurities; + if (profileItem.Security.IsNullOrEmpty()) + { + profileItem.Security = Global.DefaultSecurity; + } + break; + + case EConfigType.Shadowsocks: + gridSs.IsVisible = true; + cmbSecurity3.ItemsSource = AppManager.Instance.GetShadowsocksSecurities(profileItem); + break; + + case EConfigType.SOCKS: + case EConfigType.HTTP: + gridSocks.IsVisible = true; + break; + + case EConfigType.VLESS: + gridVLESS.IsVisible = true; + lstStreamSecurity.Add(Global.StreamSecurityReality); + cmbFlow5.ItemsSource = Global.Flows; + if (profileItem.Security.IsNullOrEmpty()) + { + profileItem.Security = Global.None; + } + break; + + case EConfigType.Trojan: + gridTrojan.IsVisible = true; + lstStreamSecurity.Add(Global.StreamSecurityReality); + cmbFlow6.ItemsSource = Global.Flows; + break; + + case EConfigType.Hysteria2: + gridHysteria2.IsVisible = true; + sepa2.IsVisible = false; + gridTransport.IsVisible = false; + cmbCoreType.IsEnabled = false; + cmbFingerprint.IsEnabled = false; + cmbFingerprint.SelectedValue = string.Empty; + break; + + case EConfigType.TUIC: + gridTuic.IsVisible = true; + sepa2.IsVisible = false; + gridTransport.IsVisible = false; + cmbCoreType.IsEnabled = false; + cmbFingerprint.IsEnabled = false; + cmbFingerprint.SelectedValue = string.Empty; + + cmbHeaderType8.ItemsSource = Global.TuicCongestionControls; + break; + + case EConfigType.WireGuard: + gridWireguard.IsVisible = true; + + sepa2.IsVisible = false; + gridTransport.IsVisible = false; + gridTls.IsVisible = false; + + break; + + case EConfigType.Anytls: + gridAnytls.IsVisible = true; + lstStreamSecurity.Add(Global.StreamSecurityReality); + cmbCoreType.IsEnabled = false; + break; + } + cmbStreamSecurity.ItemsSource = lstStreamSecurity; + + gridTlsMore.IsVisible = false; + + this.WhenActivated(disposables => + { + this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Address, v => v.txtAddress.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Port, v => v.txtPort.Text).DisposeWith(disposables); + + switch (profileItem.ConfigType) + { + case EConfigType.VMess: + this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.AlterId, v => v.txtAlterId.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.cmbSecurity.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(disposables); + break; + + case EConfigType.Shadowsocks: + this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId3.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.cmbSecurity3.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled3.IsChecked).DisposeWith(disposables); + break; + + case EConfigType.SOCKS: + case EConfigType.HTTP: + this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId4.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.txtSecurity4.Text).DisposeWith(disposables); + break; + + case EConfigType.VLESS: + this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId5.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Flow, v => v.cmbFlow5.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.txtSecurity5.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled5.IsChecked).DisposeWith(disposables); + break; + + case EConfigType.Trojan: + this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId6.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Flow, v => v.cmbFlow6.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled6.IsChecked).DisposeWith(disposables); + break; + + case EConfigType.Hysteria2: + this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Path, v => v.txtPath7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Ports, v => v.txtPorts7.Text).DisposeWith(disposables); + break; + + case EConfigType.TUIC: + this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId8.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.txtSecurity8.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.HeaderType, v => v.cmbHeaderType8.SelectedValue).DisposeWith(disposables); + break; + + case EConfigType.WireGuard: + this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.PublicKey, v => v.txtPublicKey9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Path, v => v.txtPath9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.RequestHost, v => v.txtRequestHost9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.ShortId, v => v.txtShortId9.Text).DisposeWith(disposables); + break; + + case EConfigType.Anytls: + this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId10.Text).DisposeWith(disposables); + break; + } + this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.HeaderType, v => v.cmbHeaderType.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.RequestHost, v => v.txtRequestHost.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Path, v => v.txtPath.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Extra, v => v.txtExtra.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.SelectedSource.StreamSecurity, v => v.cmbStreamSecurity.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.SelectedValue).DisposeWith(disposables); + //reality + this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.PublicKey, v => v.txtPublicKey.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.ShortId, v => v.txtShortId.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.SpiderX, v => v.txtSpiderX.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Mldsa65Verify, v => v.txtMldsa65Verify.Text).DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); + }); + + this.Title = $"{profileItem.ConfigType}"; + } + + private async Task UpdateViewHandler(EViewAction action, object? obj) + { + switch (action) + { + case EViewAction.CloseWindow: + this.Close(true); + break; + } + return await Task.FromResult(true); + } + + private void Window_Loaded(object? sender, RoutedEventArgs e) + { + txtRemarks.Focus(); + } + + private void CmbNetwork_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + SetHeaderType(); + SetTips(); + } + + private void CmbStreamSecurity_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + var security = cmbStreamSecurity.SelectedItem.ToString(); + if (security == Global.StreamSecurityReality) + { + gridRealityMore.IsVisible = true; + gridTlsMore.IsVisible = false; + } + else if (security == Global.StreamSecurity) + { + gridRealityMore.IsVisible = false; + gridTlsMore.IsVisible = true; + } + else + { + gridRealityMore.IsVisible = false; + gridTlsMore.IsVisible = false; + } + } + + private void btnGUID_Click(object? sender, RoutedEventArgs e) + { + txtId.Text = + txtId5.Text = Utils.GetGuid(); + } + + private void SetHeaderType() + { + var lstHeaderType = new List(); + + var network = cmbNetwork.SelectedItem.ToString(); + if (network.IsNullOrEmpty()) + { + lstHeaderType.Add(Global.None); + cmbHeaderType.ItemsSource = lstHeaderType; + cmbHeaderType.SelectedIndex = 0; + return; + } + + if (network == nameof(ETransport.tcp)) + { + lstHeaderType.Add(Global.None); + lstHeaderType.Add(Global.TcpHeaderHttp); + } + else if (network is nameof(ETransport.kcp) or nameof(ETransport.quic)) + { + lstHeaderType.Add(Global.None); + lstHeaderType.AddRange(Global.KcpHeaderTypes); + } + else if (network is nameof(ETransport.xhttp)) + { + lstHeaderType.AddRange(Global.XhttpMode); + } + else if (network == nameof(ETransport.grpc)) + { + lstHeaderType.Add(Global.GrpcGunMode); + lstHeaderType.Add(Global.GrpcMultiMode); + } + else + { + lstHeaderType.Add(Global.None); + } + cmbHeaderType.ItemsSource = lstHeaderType; + cmbHeaderType.SelectedIndex = 0; + } + + private void SetTips() + { + var network = cmbNetwork.SelectedItem.ToString(); + if (network.IsNullOrEmpty()) + { + network = Global.DefaultNetwork; + } + labHeaderType.IsVisible = true; + btnExtra.IsVisible = false; + tipRequestHost.Text = + tipPath.Text = + tipHeaderType.Text = string.Empty; + + switch (network) + { + case nameof(ETransport.tcp): + tipRequestHost.Text = ResUI.TransportRequestHostTip1; + tipHeaderType.Text = ResUI.TransportHeaderTypeTip1; + break; + + case nameof(ETransport.kcp): + tipHeaderType.Text = ResUI.TransportHeaderTypeTip2; + tipPath.Text = ResUI.TransportPathTip5; + break; + + case nameof(ETransport.ws): + case nameof(ETransport.httpupgrade): + tipRequestHost.Text = ResUI.TransportRequestHostTip2; + tipPath.Text = ResUI.TransportPathTip1; + break; + + case nameof(ETransport.xhttp): + tipRequestHost.Text = ResUI.TransportRequestHostTip2; + tipPath.Text = ResUI.TransportPathTip1; + tipHeaderType.Text = ResUI.TransportHeaderTypeTip5; + labHeaderType.IsVisible = false; + btnExtra.IsVisible = true; + break; + + case nameof(ETransport.h2): + tipRequestHost.Text = ResUI.TransportRequestHostTip3; + tipPath.Text = ResUI.TransportPathTip2; + break; + + case nameof(ETransport.quic): + tipRequestHost.Text = ResUI.TransportRequestHostTip4; + tipPath.Text = ResUI.TransportPathTip3; + tipHeaderType.Text = ResUI.TransportHeaderTypeTip3; + break; + + case nameof(ETransport.grpc): + tipRequestHost.Text = ResUI.TransportRequestHostTip5; + tipPath.Text = ResUI.TransportPathTip4; + tipHeaderType.Text = ResUI.TransportHeaderTypeTip4; + labHeaderType.IsVisible = false; + break; + } + } +} diff --git a/v2rayN/v2rayN.Desktop/Views/BackupAndRestoreView.axaml b/v2rayN/v2rayN.Desktop/Views/BackupAndRestoreView.axaml new file mode 100644 index 00000000..7130c400 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/BackupAndRestoreView.axaml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs new file mode 100644 index 00000000..76a0fd1d --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs @@ -0,0 +1,60 @@ +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace v2rayN.Desktop.Views; + +public partial class ClashConnectionsView : ReactiveUserControl +{ + public ClashConnectionsView() + { + InitializeComponent(); + ViewModel = new ClashConnectionsViewModel(UpdateViewHandler); + btnAutofitColumnWidth.Click += BtnAutofitColumnWidth_Click; + + this.WhenActivated(disposables => + { + this.OneWayBind(ViewModel, vm => vm.ConnectionItems, v => v.lstConnections.ItemsSource).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource, v => v.lstConnections.SelectedItem).DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.ConnectionCloseCmd, v => v.menuConnectionClose).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.ConnectionCloseAllCmd, v => v.menuConnectionCloseAll).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.HostFilter, v => v.txtHostFilter.Text).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.ConnectionCloseAllCmd, v => v.btnConnectionCloseAll).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); + }); + } + + private async Task UpdateViewHandler(EViewAction action, object? obj) + { + return await Task.FromResult(true); + } + + private void BtnAutofitColumnWidth_Click(object? sender, RoutedEventArgs e) + { + AutofitColumnWidth(); + } + + private void AutofitColumnWidth() + { + try + { + foreach (var it in lstConnections.Columns) + { + it.Width = new DataGridLength(1, DataGridLengthUnitType.Auto); + } + } + catch (Exception ex) + { + Logging.SaveLog("ClashConnectionsView", ex); + } + } + + private void btnClose_Click(object? sender, RoutedEventArgs e) + { + ViewModel?.ClashConnectionClose(false); + } +} diff --git a/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml b/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml new file mode 100644 index 00000000..974d5192 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml.cs new file mode 100644 index 00000000..754dd2a1 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml.cs @@ -0,0 +1,63 @@ +using System.Reactive.Disposables; +using Avalonia.Input; +using Avalonia.ReactiveUI; +using DynamicData; +using ReactiveUI; +using Splat; + +namespace v2rayN.Desktop.Views; + +public partial class ClashProxiesView : ReactiveUserControl +{ + public ClashProxiesView() + { + InitializeComponent(); + ViewModel = new ClashProxiesViewModel(UpdateViewHandler); + Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(ClashProxiesViewModel)); + lstProxyDetails.DoubleTapped += LstProxyDetails_DoubleTapped; + this.KeyDown += ClashProxiesView_KeyDown; + + this.WhenActivated(disposables => + { + this.OneWayBind(ViewModel, vm => vm.ProxyGroups, v => v.lstProxyGroups.ItemsSource).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedGroup, v => v.lstProxyGroups.SelectedItem).DisposeWith(disposables); + + this.OneWayBind(ViewModel, vm => vm.ProxyDetails, v => v.lstProxyDetails.ItemsSource).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedDetail, v => v.lstProxyDetails.SelectedItem).DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.ProxiesReloadCmd, v => v.menuProxiesReload).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.ProxiesDelayTestCmd, v => v.menuProxiesDelaytest).DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.ProxiesDelayTestPartCmd, v => v.menuProxiesDelaytestPart).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.ProxiesSelectActivityCmd, v => v.menuProxiesSelectActivity).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.RuleModeSelected, v => v.cmbRulemode.SelectedIndex).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SortingSelected, v => v.cmbSorting.SelectedIndex).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); + }); + } + + private async Task UpdateViewHandler(EViewAction action, object? obj) + { + return await Task.FromResult(true); + } + + private void ClashProxiesView_KeyDown(object? sender, KeyEventArgs e) + { + switch (e.Key) + { + case Key.F5: + ViewModel?.ProxiesReload(); + break; + + case Key.Enter: + ViewModel?.SetActiveProxy(); + break; + } + } + + private void LstProxyDetails_DoubleTapped(object? sender, Avalonia.Input.TappedEventArgs e) + { + ViewModel?.SetActiveProxy(); + } +} diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml new file mode 100644 index 00000000..18f294d3 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml @@ -0,0 +1,479 @@ + + + +