diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/01_bug_report.yml index e96682bd..a9a4753e 100644 --- a/.github/ISSUE_TEMPLATE/01_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/01_bug_report.yml @@ -3,6 +3,18 @@ description: 在提出问题前请先自行排除服务器端问题和升级到 title: "[Bug]: " labels: ["bug"] body: + - type: markdown + attributes: + value: | + ### 报告 Bug 前请务必确认以下事项: + > ** ** + > **✓ 已自行排除服务器端问题。** + > **✓ 已升级到最新客户端版本。** + > **✓ 已通过搜索确认没有人提出过相同问题。** + > **✓ 已确认自己的电脑系统环境是受支持的。** + + --- + - type: input id: "os-version" attributes: @@ -10,6 +22,7 @@ body: description: "操作系统和版本" validations: required: true + - type: input id: "expectation" attributes: @@ -17,6 +30,7 @@ body: description: "描述你认为应该发生什么" validations: required: true + - type: textarea id: "describe-the-bug" attributes: @@ -24,22 +38,34 @@ body: description: "描述实际发生了什么" validations: required: true + - type: textarea id: "reproduction-method" attributes: label: "复现方法" description: "在BUG出现前执行了哪些操作" - placeholder: 标序号 + placeholder: "标序号" validations: required: true + - type: textarea - id: "log" + id: "gui-log" attributes: - label: "日志信息" + label: "软件日志" description: "位置在软件当前目录下的guiLogs" - placeholder: 在日志开始和结束位置粘贴冒号后的内容:``` + placeholder: "在日志开始和结束位置粘贴冒号后的内容到这:" validations: required: true + + - type: textarea + id: "core-log" + attributes: + label: "内核日志" + description: "位置在软件主界面的信息框内" + placeholder: "在信息框内鼠标右键复制全部信息粘贴在这:" + validations: + required: true + - type: textarea id: "more" attributes: @@ -47,6 +73,7 @@ body: description: "可选" validations: required: false + - type: checkboxes id: "latest-version" attributes: @@ -55,6 +82,7 @@ body: options: - label: 是 required: true + - type: checkboxes id: "issues" attributes: @@ -63,3 +91,12 @@ body: options: - label: 是 required: true + + - type: checkboxes + id: "system-version" + attributes: + label: "我确认系统版本是受支持的" + description: "否则请切换后尝试" + options: + - label: 是 + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..8dc9c926 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +blank_issues_enabled: false + +contact_links: + - name: Discussions / 讨论区 + url: https://github.com/2dust/v2rayN/discussions + about: 使用问题或需要帮助请前往 Discussions。 + - name: Wiki / 使用说明 + url: https://github.com/2dust/v2rayN/wiki + about: 查看常见问题和使用文档。 diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 40e9c953..b67527a1 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -9,6 +9,12 @@ on: push: branches: - master + tags: + - 'v*' + - 'V*' + +permissions: + contents: write env: OutputArch: "linux-64" @@ -21,8 +27,7 @@ jobs: strategy: matrix: configuration: [Release] - - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout @@ -31,21 +36,21 @@ jobs: submodules: 'recursive' fetch-depth: '0' - - name: Setup + - name: Setup .NET 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 + 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 + uses: actions/upload-artifact@v5.0.0 with: name: v2rayN-linux path: | @@ -56,8 +61,8 @@ jobs: 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 }} + ./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 @@ -68,28 +73,13 @@ jobs: 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 + ./package-release-zip.sh "$OutputArch" "$OutputPath64" + ./package-release-zip.sh "$OutputArchArm" "$OutputPathArm64" - name: Upload zip archive to release uses: svenstaro/upload-release-action@v2 @@ -100,36 +90,62 @@ jobs: file_glob: true prerelease: true - # release RHEL package - - name: Package RPM (RHEL-family) - if: github.event.inputs.release_tag != '' + rpm: + needs: build + if: | + (github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != '') || + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) + runs-on: ubuntu-24.04 + container: + image: quay.io/almalinuxorg/10-base:latest + options: --platform=linux/amd64/v2 + env: + RELEASE_TAG: ${{ github.event.inputs.release_tag != '' && github.event.inputs.release_tag || github.ref_name }} + + steps: + - name: Prepare tools (Red Hat) 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 + dnf -y makecache + dnf -y install epel-release + dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which + + - name: Checkout repo (for scripts) + uses: actions/checkout@v5.0.0 + with: + submodules: 'recursive' + fetch-depth: '0' + + - name: Restore build artifacts + uses: actions/download-artifact@v6 + with: + name: v2rayN-linux + path: ${{ github.workspace }}/v2rayN/Release + + - name: Ensure script permissions + run: chmod 755 package-rhel.sh + + - name: Package RPM (RHEL-family) + run: ./package-rhel.sh "${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 + mkdir -p "$GITHUB_WORKSPACE/dist/rpm" + rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm/" || true + find "$GITHUB_WORKSPACE/dist/rpm" -name "v2rayN-*-1*.x86_64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm/v2rayN-linux-rhel-64.rpm" \; || true + find "$GITHUB_WORKSPACE/dist/rpm" -name "v2rayN-*-1*.aarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm/v2rayN-linux-rhel-arm64.rpm" \; || true + echo "==== Dist tree ====" + ls -R "$GITHUB_WORKSPACE/dist/rpm" || true - name: Upload RPM artifacts - if: github.event.inputs.release_tag != '' - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@v5.0.0 with: name: v2rayN-rpm - path: | - ${{ github.workspace }}/dist/rpm/**/*.rpm + path: 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: dist/rpm/**/*.rpm + tag: ${{ env.RELEASE_TAG }} file_glob: true prerelease: true diff --git a/.github/workflows/build-osx.yml b/.github/workflows/build-osx.yml index 97c002c7..5825ac9d 100644 --- a/.github/workflows/build-osx.yml +++ b/.github/workflows/build-osx.yml @@ -45,7 +45,7 @@ jobs: 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 + uses: actions/upload-artifact@v5.0.0 with: name: v2rayN-macos path: | diff --git a/.github/workflows/build-windows-desktop.yml b/.github/workflows/build-windows-desktop.yml index 3b28599d..65b86126 100644 --- a/.github/workflows/build-windows-desktop.yml +++ b/.github/workflows/build-windows-desktop.yml @@ -45,7 +45,7 @@ jobs: 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 + uses: actions/upload-artifact@v5.0.0 with: name: v2rayN-windows-desktop path: | diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index fea3aa70..31b5a90c 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -46,7 +46,7 @@ jobs: - name: Upload build artifacts - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@v5.0.0 with: name: v2rayN-windows path: | diff --git a/package-appimage.sh b/package-appimage.sh deleted file mode 100644 index 6a8bfcca..00000000 --- a/package-appimage.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/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" <= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1) Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others EOF diff --git a/package-osx.sh b/package-osx.sh index 838c3bbf..a3830bc5 100755 --- a/package-osx.sh +++ b/package-osx.sh @@ -43,6 +43,8 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF NSHighResolutionCapable + LSMinimumSystemVersion + 12.7 EOF @@ -55,4 +57,4 @@ create-dmg \ --hide-extension "v2rayN.app" \ --app-drop-link 500 185 \ "v2rayN-${Arch}.dmg" \ - "$PackagePath/v2rayN.app" \ No newline at end of file + "$PackagePath/v2rayN.app" diff --git a/package-rhel.sh b/package-rhel.sh index ea537c62..b93ea82c 100644 --- a/package-rhel.sh +++ b/package-rhel.sh @@ -19,6 +19,23 @@ else exit 1 fi +# ======================== Kernel version check (require >= 6.11) ======================= +MIN_KERNEL_MAJOR=6 +MIN_KERNEL_MINOR=11 +KERNEL_FULL=$(uname -r) +KERNEL_MAJOR=$(echo "$KERNEL_FULL" | cut -d. -f1) +KERNEL_MINOR=$(echo "$KERNEL_FULL" | cut -d. -f2) + +echo "[INFO] Detected kernel version: $KERNEL_FULL" + +if (( KERNEL_MAJOR < MIN_KERNEL_MAJOR )) || { (( KERNEL_MAJOR == MIN_KERNEL_MAJOR )) && (( KERNEL_MINOR < MIN_KERNEL_MINOR )); }; then + echo "[ERROR] Kernel $KERNEL_FULL is too old. Requires Linux >= ${MIN_KERNEL_MAJOR}.${MIN_KERNEL_MINOR}." + echo "Please upgrade your system or use a newer container (e.g. Fedora 42+, RHEL 10+, Debian 13+)." + exit 1 +fi + +echo "[OK] Kernel version >= ${MIN_KERNEL_MAJOR}.${MIN_KERNEL_MINOR}." + # ===== 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 @@ -614,8 +631,13 @@ 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 +Requires: freetype, cairo, pango, openssl, mesa-libEGL, mesa-libGL +Requires: glibc >= 2.34 +Requires: fontconfig >= 2.13.1 +Requires: desktop-file-utils >= 0.26 +Requires: xdg-utils >= 1.1.3 +Requires: coreutils >= 8.32 +Requires: bash >= 5.1 %description v2rayN Linux for Red Hat Enterprise Linux diff --git a/v2rayN/Directory.Build.props b/v2rayN/Directory.Build.props index 7bae47f6..2e863e42 100644 --- a/v2rayN/Directory.Build.props +++ b/v2rayN/Directory.Build.props @@ -1,14 +1,14 @@ - 7.14.10 + 7.16.1 net8.0 true true - CA1031;CS1591;NU1507;CA1416;IDE0058 + CA1031;CS1591;NU1507;CA1416;IDE0058;IDE0053;IDE0200 annotations enable 2dust diff --git a/v2rayN/Directory.Packages.props b/v2rayN/Directory.Packages.props index 6f8fadf2..1e4f9e33 100644 --- a/v2rayN/Directory.Packages.props +++ b/v2rayN/Directory.Packages.props @@ -5,22 +5,24 @@ false - - - - + + + + + - - - - - + + + + + - - - - + + + + + diff --git a/v2rayN/GlobalHotKeys b/v2rayN/GlobalHotKeys index 270f023f..ffb2850d 160000 --- a/v2rayN/GlobalHotKeys +++ b/v2rayN/GlobalHotKeys @@ -1 +1 @@ -Subproject commit 270f023fcca4ef9149480c575d502815efd83531 +Subproject commit ffb2850df0991495d0918e13cc5701737f26175a diff --git a/v2rayN/ServiceLib/Base/MyReactiveObject.cs b/v2rayN/ServiceLib/Base/MyReactiveObject.cs index 0172faad..50563151 100644 --- a/v2rayN/ServiceLib/Base/MyReactiveObject.cs +++ b/v2rayN/ServiceLib/Base/MyReactiveObject.cs @@ -1,5 +1,3 @@ -using ReactiveUI; - namespace ServiceLib.Base; public class MyReactiveObject : ReactiveObject diff --git a/v2rayN/ServiceLib/Common/EmbedUtils.cs b/v2rayN/ServiceLib/Common/EmbedUtils.cs index b647d154..d3b323f7 100644 --- a/v2rayN/ServiceLib/Common/EmbedUtils.cs +++ b/v2rayN/ServiceLib/Common/EmbedUtils.cs @@ -1,6 +1,3 @@ -using System.Collections.Concurrent; -using System.Reflection; - namespace ServiceLib.Common; public static class EmbedUtils diff --git a/v2rayN/ServiceLib/Common/Extension.cs b/v2rayN/ServiceLib/Common/Extension.cs index 49d59534..d36b8169 100644 --- a/v2rayN/ServiceLib/Common/Extension.cs +++ b/v2rayN/ServiceLib/Common/Extension.cs @@ -84,4 +84,14 @@ public static class Extension { return source.Concat(new[] { string.Empty }).ToList(); } + + public static bool IsGroupType(this EConfigType configType) + { + return configType is EConfigType.PolicyGroup or EConfigType.ProxyChain; + } + + public static bool IsComplexType(this EConfigType configType) + { + return configType is EConfigType.Custom or EConfigType.PolicyGroup or EConfigType.ProxyChain; + } } diff --git a/v2rayN/ServiceLib/Common/FileManager.cs b/v2rayN/ServiceLib/Common/FileUtils.cs similarity index 99% rename from v2rayN/ServiceLib/Common/FileManager.cs rename to v2rayN/ServiceLib/Common/FileUtils.cs index 6d4d28ca..2f86b60a 100644 --- a/v2rayN/ServiceLib/Common/FileManager.cs +++ b/v2rayN/ServiceLib/Common/FileUtils.cs @@ -1,10 +1,9 @@ using System.Formats.Tar; using System.IO.Compression; -using System.Text; namespace ServiceLib.Common; -public static class FileManager +public static class FileUtils { private static readonly string _tag = "FileManager"; diff --git a/v2rayN/ServiceLib/Common/Job.cs b/v2rayN/ServiceLib/Common/Job.cs deleted file mode 100644 index fe968d2d..00000000 --- a/v2rayN/ServiceLib/Common/Job.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace ServiceLib.Common; - /* - * See: - * http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net - */ - - public sealed class Job : IDisposable - { - private IntPtr handle = IntPtr.Zero; - - public Job() - { - handle = CreateJobObject(IntPtr.Zero, null); - var extendedInfoPtr = IntPtr.Zero; - var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION - { - LimitFlags = 0x2000 - }; - - var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION - { - BasicLimitInformation = info - }; - - try - { - var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); - extendedInfoPtr = Marshal.AllocHGlobal(length); - Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); - - if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, - (uint)length)) - { - throw new Exception(string.Format("Unable to set information. Error: {0}", - Marshal.GetLastWin32Error())); - } - } - finally - { - if (extendedInfoPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(extendedInfoPtr); - } - } - } - - public bool AddProcess(IntPtr processHandle) - { - var succ = AssignProcessToJobObject(handle, processHandle); - - if (!succ) - { - Logging.SaveLog("Failed to call AssignProcessToJobObject! GetLastError=" + Marshal.GetLastWin32Error()); - } - - return succ; - } - - public bool AddProcess(int processId) - { - return AddProcess(Process.GetProcessById(processId).Handle); - } - - #region IDisposable - - private bool disposed; - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposed) - { - return; - } - disposed = true; - - if (disposing) - { - // no managed objects to free - } - - if (handle != IntPtr.Zero) - { - CloseHandle(handle); - handle = IntPtr.Zero; - } - } - - ~Job() - { - Dispose(false); - } - - #endregion IDisposable - - #region Interop - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] - private static extern IntPtr CreateJobObject(IntPtr a, string? lpName); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); - - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool CloseHandle(IntPtr hObject); - - #endregion Interop - } - - #region Helper classes - - [StructLayout(LayoutKind.Sequential)] - internal struct IO_COUNTERS - { - public ulong ReadOperationCount; - public ulong WriteOperationCount; - public ulong OtherOperationCount; - public ulong ReadTransferCount; - public ulong WriteTransferCount; - public ulong OtherTransferCount; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct JOBOBJECT_BASIC_LIMIT_INFORMATION - { - public long PerProcessUserTimeLimit; - public long PerJobUserTimeLimit; - public uint LimitFlags; - public UIntPtr MinimumWorkingSetSize; - public UIntPtr MaximumWorkingSetSize; - public uint ActiveProcessLimit; - public UIntPtr Affinity; - public uint PriorityClass; - public uint SchedulingClass; - } - - [StructLayout(LayoutKind.Sequential)] - public struct SECURITY_ATTRIBUTES - { - public uint nLength; - public IntPtr lpSecurityDescriptor; - public int bInheritHandle; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION - { - public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; - public IO_COUNTERS IoInfo; - public UIntPtr ProcessMemoryLimit; - public UIntPtr JobMemoryLimit; - public UIntPtr PeakProcessMemoryUsed; - public UIntPtr PeakJobMemoryUsed; - } - - public enum JobObjectInfoType - { - AssociateCompletionPortInformation = 7, - BasicLimitInformation = 2, - BasicUIRestrictions = 4, - EndOfJobTimeInformation = 6, - ExtendedLimitInformation = 9, - SecurityLimitInformation = 5, - GroupInformation = 11 - } - - #endregion Helper classes - diff --git a/v2rayN/ServiceLib/Common/JsonUtils.cs b/v2rayN/ServiceLib/Common/JsonUtils.cs index 4fbaa0e1..7e2b7f78 100644 --- a/v2rayN/ServiceLib/Common/JsonUtils.cs +++ b/v2rayN/ServiceLib/Common/JsonUtils.cs @@ -1,8 +1,3 @@ -using System.Text.Encodings.Web; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - namespace ServiceLib.Common; public class JsonUtils @@ -40,9 +35,13 @@ public class JsonUtils /// /// /// - public static T DeepCopy(T obj) + public static T? DeepCopy(T? obj) { - return Deserialize(Serialize(obj, false))!; + if (obj is null) + { + return default; + } + return Deserialize(Serialize(obj, false)); } /// @@ -72,7 +71,7 @@ public class JsonUtils /// /// /// - public static JsonNode? ParseJson(string strJson) + public static JsonNode? ParseJson(string? strJson) { try { @@ -121,7 +120,7 @@ public class JsonUtils /// /// /// - public static string Serialize(object? obj, JsonSerializerOptions options) + public static string Serialize(object? obj, JsonSerializerOptions? options) { var result = string.Empty; try @@ -130,7 +129,7 @@ public class JsonUtils { return result; } - result = JsonSerializer.Serialize(obj, options); + result = JsonSerializer.Serialize(obj, options ?? _defaultSerializeOptions); } catch (Exception ex) { diff --git a/v2rayN/ServiceLib/Common/ProcUtils.cs b/v2rayN/ServiceLib/Common/ProcUtils.cs index 561278ce..ce487c7a 100644 --- a/v2rayN/ServiceLib/Common/ProcUtils.cs +++ b/v2rayN/ServiceLib/Common/ProcUtils.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; - namespace ServiceLib.Common; public static class ProcUtils @@ -67,116 +65,4 @@ public static class ProcUtils Logging.SaveLog(_tag, ex); } } - - public static async Task ProcessKill(int pid) - { - try - { - await ProcessKill(Process.GetProcessById(pid), false); - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - } - - public static async Task ProcessKill(Process? proc, bool review) - { - if (proc is null) - { - return; - } - - GetProcessKeyInfo(proc, review, out var procId, out var fileName, out var processName); - - try - { - if (Utils.IsNonWindows()) - { - proc?.Kill(true); - } - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - - try - { - proc?.Kill(); - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - - try - { - proc?.Close(); - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - - try - { - proc?.Dispose(); - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - - await Task.Delay(300); - await ProcessKillByKeyInfo(review, procId, fileName, processName); - } - - private static void GetProcessKeyInfo(Process? proc, bool review, out int? procId, out string? fileName, out string? processName) - { - procId = null; - fileName = null; - processName = null; - if (!review) - { - return; - } - try - { - procId = proc?.Id; - fileName = proc?.MainModule?.FileName; - processName = proc?.ProcessName; - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - } - - private static async Task ProcessKillByKeyInfo(bool review, int? procId, string? fileName, string? processName) - { - if (review && procId != null && fileName != null) - { - try - { - var lstProc = Process.GetProcessesByName(processName); - foreach (var proc2 in lstProc) - { - if (proc2.Id == procId) - { - Logging.SaveLog($"{_tag}, KillProcess not completing the job, procId"); - await ProcessKill(proc2, false); - } - if (proc2.MainModule != null && proc2.MainModule?.FileName == fileName) - { - Logging.SaveLog($"{_tag}, KillProcess not completing the job, fileName"); - } - } - } - catch (Exception ex) - { - Logging.SaveLog(_tag, ex); - } - } - } } diff --git a/v2rayN/ServiceLib/Common/QRCodeUtils.cs b/v2rayN/ServiceLib/Common/QRCodeUtils.cs index 3d3dc90b..d9015367 100644 --- a/v2rayN/ServiceLib/Common/QRCodeUtils.cs +++ b/v2rayN/ServiceLib/Common/QRCodeUtils.cs @@ -1,4 +1,5 @@ using QRCoder; +using QRCoder.Exceptions; using SkiaSharp; using ZXing.SkiaSharp; @@ -8,10 +9,45 @@ public class QRCodeUtils { public static byte[]? GenQRCode(string? url) { + if (url.IsNullOrEmpty()) + { + return null; + } using QRCodeGenerator qrGenerator = new(); - using var qrCodeData = qrGenerator.CreateQrCode(url ?? string.Empty, QRCodeGenerator.ECCLevel.Q); - using PngByteQRCode qrCode = new(qrCodeData); - return qrCode.GetGraphic(20); + DataTooLongException? lastDtle = null; + + var levels = new[] + { + QRCodeGenerator.ECCLevel.H, + QRCodeGenerator.ECCLevel.Q, + QRCodeGenerator.ECCLevel.M, + QRCodeGenerator.ECCLevel.L + }; + foreach (var level in levels) + { + try + { + using var qrCodeData = qrGenerator.CreateQrCode(url, level); + using PngByteQRCode qrCode = new(qrCodeData); + return qrCode.GetGraphic(20); + } + catch (DataTooLongException ex) + { + lastDtle = ex; + continue; + } + catch + { + throw; + } + } + + if (lastDtle != null) + { + throw lastDtle; + } + + return null; } public static string? ParseBarcode(string? fileName) diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index 358d4e9a..1712b40c 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -1,13 +1,5 @@ using System.Collections.Specialized; -using System.Diagnostics; -using System.Net; -using System.Net.NetworkInformation; -using System.Net.Sockets; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Security.Cryptography; using System.Security.Principal; -using System.Text; using CliWrap; using CliWrap.Buffered; @@ -17,7 +9,7 @@ public class Utils { private static readonly string _tag = "Utils"; - #region 转换函数 + #region Conversion Functions /// /// Convert to comma-separated string @@ -85,13 +77,19 @@ public class Utils /// Base64 Encode /// /// + /// /// - public static string Base64Encode(string plainText) + public static string Base64Encode(string plainText, bool removePadding = false) { try { var plainTextBytes = Encoding.UTF8.GetBytes(plainText); - return Convert.ToBase64String(plainTextBytes); + var base64 = Convert.ToBase64String(plainTextBytes); + if (removePadding) + { + base64 = base64.TrimEnd('='); + } + return base64; } catch (Exception ex) { @@ -112,7 +110,7 @@ public class Utils { if (plainText.IsNullOrEmpty()) { - return ""; + return string.Empty; } plainText = plainText.Trim() @@ -308,7 +306,10 @@ public class Utils public static bool IsBase64String(string? plainText) { if (plainText.IsNullOrEmpty()) + { return false; + } + var buffer = new Span(new byte[plainText.Length]); return Convert.TryFromBase64String(plainText, buffer, out var _); } @@ -357,9 +358,113 @@ public class Utils return userHostsMap; } - #endregion 转换函数 + /// + /// Parse a possibly non-standard URL into scheme, domain, port, and path. + /// If parsing fails, the entire input is returned as domain, and others are empty or zero. + /// + /// Input URL or string + /// (domain, scheme, port, path) + public static (string domain, string scheme, int port, string path) ParseUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) + { + return ("", "", 0, ""); + } - #region 数据检查 + // 1. First, try to parse using the standard Uri class. + if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && !string.IsNullOrEmpty(uri.Host)) + { + var scheme = uri.Scheme; + var domain = uri.Host; + var port = uri.IsDefaultPort ? 0 : uri.Port; + var path = uri.PathAndQuery; + return (domain, scheme, port, path); + } + + // 2. Try to handle more general cases with a regular expression, including non-standard schemes. + // This regex captures the scheme (optional), authority (host+port), and path (optional). + var match = Regex.Match(url, @"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):/{2,})?([^/?#]+)([^?#]*)?.*$"); + + if (match.Success) + { + var scheme = match.Groups[1].Value; + var authority = match.Groups[2].Value; + var path = match.Groups[3].Value; + + // Remove userinfo from the authority part. + var atIndex = authority.LastIndexOf('@'); + if (atIndex > 0) + { + authority = authority.Substring(atIndex + 1); + } + + var (domain, port) = ParseAuthority(authority); + + // If the parsed domain is empty, it means the authority part is malformed, so trigger the fallback. + if (!string.IsNullOrEmpty(domain)) + { + return (domain, scheme, port, path); + } + } + + // 3. If all of the above fails, execute the final fallback strategy. + return (url, "", 0, ""); + } + + /// + /// Helper function to parse domain and port from the authority part, with correct handling for IPv6. + /// + private static (string domain, int port) ParseAuthority(string authority) + { + if (string.IsNullOrEmpty(authority)) + { + return ("", 0); + } + + var port = 0; + var domain = authority; + + // Handle IPv6 addresses, e.g., "[2001:db8::1]:443" + if (authority.StartsWith("[") && authority.Contains("]")) + { + var closingBracketIndex = authority.LastIndexOf(']'); + if (closingBracketIndex < authority.Length - 1 && authority[closingBracketIndex + 1] == ':') + { + // Port exists + var portStr = authority.Substring(closingBracketIndex + 2); + if (int.TryParse(portStr, out var portNum)) + { + port = portNum; + } + domain = authority.Substring(0, closingBracketIndex + 1); + } + else + { + // No port + domain = authority; + } + } + else // Handle IPv4 or domain names + { + var lastColonIndex = authority.LastIndexOf(':'); + // Ensure there are digits after the colon and that this colon is not part of an IPv6 address. + if (lastColonIndex > 0 && lastColonIndex < authority.Length - 1 && authority.Substring(lastColonIndex + 1).All(char.IsDigit)) + { + var portStr = authority.Substring(lastColonIndex + 1); + if (int.TryParse(portStr, out var portNum)) + { + port = portNum; + domain = authority.Substring(0, lastColonIndex); + } + } + } + + return (domain, port); + } + + #endregion Conversion Functions + + #region Data Checks /// /// Determine if the input is a number @@ -418,40 +523,62 @@ public class Utils { // Loopback address check (127.0.0.1 for IPv4, ::1 for IPv6) if (IPAddress.IsLoopback(address)) + { return true; + } var ipBytes = address.GetAddressBytes(); if (address.AddressFamily == AddressFamily.InterNetwork) { // IPv4 private address check if (ipBytes[0] == 10) + { return true; + } + if (ipBytes[0] == 172 && ipBytes[1] >= 16 && ipBytes[1] <= 31) + { return true; + } + if (ipBytes[0] == 192 && ipBytes[1] == 168) + { return true; + } } else if (address.AddressFamily == AddressFamily.InterNetworkV6) { // IPv6 private address check // Link-local address fe80::/10 if (ipBytes[0] == 0xfe && (ipBytes[1] & 0xc0) == 0x80) + { return true; + } // Unique local address fc00::/7 (typically fd00::/8) if ((ipBytes[0] & 0xfe) == 0xfc) + { return true; + } // Private portion in IPv4-mapped addresses ::ffff:0:0/96 if (address.IsIPv4MappedToIPv6) { var ipv4Bytes = ipBytes.Skip(12).ToArray(); if (ipv4Bytes[0] == 10) + { return true; + } + if (ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31) + { return true; + } + if (ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168) + { return true; + } } } } @@ -459,9 +586,9 @@ public class Utils return false; } - #endregion 数据检查 + #endregion Data Checks - #region 测速 + #region Speed Test private static bool PortInUse(int port) { @@ -514,9 +641,9 @@ public class Utils return 59090; } - #endregion 测速 + #endregion Speed Test - #region 杂项 + #region Miscellaneous public static bool UpgradeAppExists(out string upgradeFileName) { @@ -606,10 +733,16 @@ public class Utils foreach (var host in hostsList) { if (host.StartsWith("#")) + { continue; + } + var hostItem = host.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); if (hostItem.Length < 2) + { continue; + } + systemHosts.Add(hostItem[1], hostItem[0]); } } @@ -660,7 +793,7 @@ public class Utils return null; } - #endregion 杂项 + #endregion Miscellaneous #region TempPath @@ -861,13 +994,13 @@ public class Utils #region Platform - public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + public static bool IsWindows() => OperatingSystem.IsWindows(); - public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + public static bool IsLinux() => OperatingSystem.IsLinux(); - public static bool IsOSX() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + public static bool IsMacOS() => OperatingSystem.IsMacOS(); - public static bool IsNonWindows() => !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + public static bool IsNonWindows() => !OperatingSystem.IsWindows(); public static string GetExeName(string name) { @@ -887,16 +1020,11 @@ public class Utils { try { - if (IsWindows() || IsOSX()) + if (IsWindows() || IsMacOS()) { return false; } - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPIMAGE"))) - { - return true; - } - var exePath = GetExePath(); var baseDir = string.IsNullOrEmpty(exePath) ? StartupPath() : Path.GetDirectoryName(exePath) ?? ""; var p = baseDir.Replace('\\', '/'); @@ -906,11 +1034,6 @@ public class Utils return false; } - if (p.Contains("/.mount_", StringComparison.Ordinal)) - { - return true; - } - if (p.StartsWith("/opt/v2rayN", StringComparison.OrdinalIgnoreCase)) { return true; @@ -947,7 +1070,7 @@ public class Utils if (SetUnixFileMode(fileName)) { Logging.SaveLog($"Successfully set the file execution permission, {fileName}"); - return ""; + return string.Empty; } if (fileName.Contains(' ')) diff --git a/v2rayN/ServiceLib/Common/WindowsUtils.cs b/v2rayN/ServiceLib/Common/WindowsUtils.cs index 6c6f7be1..2215a3e3 100644 --- a/v2rayN/ServiceLib/Common/WindowsUtils.cs +++ b/v2rayN/ServiceLib/Common/WindowsUtils.cs @@ -1,5 +1,3 @@ -using System.Security.Cryptography; -using System.Text; using Microsoft.Win32; namespace ServiceLib.Common; diff --git a/v2rayN/ServiceLib/Enums/EConfigType.cs b/v2rayN/ServiceLib/Enums/EConfigType.cs index 6698f962..287d7b94 100644 --- a/v2rayN/ServiceLib/Enums/EConfigType.cs +++ b/v2rayN/ServiceLib/Enums/EConfigType.cs @@ -12,5 +12,7 @@ public enum EConfigType TUIC = 8, WireGuard = 9, HTTP = 10, - Anytls = 11 + Anytls = 11, + PolicyGroup = 101, + ProxyChain = 102, } diff --git a/v2rayN/ServiceLib/Enums/EMultipleLoad.cs b/v2rayN/ServiceLib/Enums/EMultipleLoad.cs index 42be7a5b..ef6a5ff9 100644 --- a/v2rayN/ServiceLib/Enums/EMultipleLoad.cs +++ b/v2rayN/ServiceLib/Enums/EMultipleLoad.cs @@ -2,8 +2,9 @@ namespace ServiceLib.Enums; public enum EMultipleLoad { + LeastPing, + Fallback, Random, RoundRobin, - LeastPing, LeastLoad } diff --git a/v2rayN/ServiceLib/Enums/ERuleType.cs b/v2rayN/ServiceLib/Enums/ERuleType.cs new file mode 100644 index 00000000..7455bf2a --- /dev/null +++ b/v2rayN/ServiceLib/Enums/ERuleType.cs @@ -0,0 +1,8 @@ +namespace ServiceLib.Enums; + +public enum ERuleType +{ + ALL = 0, + Routing = 1, + DNS = 2, +} diff --git a/v2rayN/ServiceLib/Enums/ESpeedActionType.cs b/v2rayN/ServiceLib/Enums/ESpeedActionType.cs index 0478bcc0..a03aa9df 100644 --- a/v2rayN/ServiceLib/Enums/ESpeedActionType.cs +++ b/v2rayN/ServiceLib/Enums/ESpeedActionType.cs @@ -5,5 +5,6 @@ public enum ESpeedActionType Tcping, Realping, Speedtest, - Mixedtest + Mixedtest, + FastRealping } diff --git a/v2rayN/ServiceLib/Enums/EViewAction.cs b/v2rayN/ServiceLib/Enums/EViewAction.cs index 2c47d31d..fba340dc 100644 --- a/v2rayN/ServiceLib/Enums/EViewAction.cs +++ b/v2rayN/ServiceLib/Enums/EViewAction.cs @@ -12,7 +12,6 @@ public enum EViewAction ProfilesFocus, ShareSub, ShareServer, - ShowHideWindow, ScanScreenTask, ScanImageTask, BrowseServer, @@ -24,6 +23,7 @@ public enum EViewAction RoutingRuleDetailsWindow, AddServerWindow, AddServer2Window, + AddGroupServerWindow, DNSSettingWindow, RoutingSettingWindow, OptionSettingWindow, diff --git a/v2rayN/ServiceLib/Events/AppEvents.cs b/v2rayN/ServiceLib/Events/AppEvents.cs new file mode 100644 index 00000000..5824bfc0 --- /dev/null +++ b/v2rayN/ServiceLib/Events/AppEvents.cs @@ -0,0 +1,30 @@ +namespace ServiceLib.Events; + +public static class AppEvents +{ + public static readonly EventChannel ReloadRequested = new(); + public static readonly EventChannel ShowHideWindowRequested = new(); + public static readonly EventChannel AddServerViaScanRequested = new(); + public static readonly EventChannel AddServerViaClipboardRequested = new(); + public static readonly EventChannel SubscriptionsUpdateRequested = new(); + + public static readonly EventChannel ProfilesRefreshRequested = new(); + public static readonly EventChannel SubscriptionsRefreshRequested = new(); + public static readonly EventChannel ProxiesReloadRequested = new(); + public static readonly EventChannel DispatcherStatisticsRequested = new(); + + public static readonly EventChannel SendSnackMsgRequested = new(); + public static readonly EventChannel SendMsgViewRequested = new(); + + public static readonly EventChannel AppExitRequested = new(); + public static readonly EventChannel ShutdownRequested = new(); + + public static readonly EventChannel AdjustMainLvColWidthRequested = new(); + + public static readonly EventChannel SetDefaultServerRequested = new(); + + public static readonly EventChannel RoutingsMenuRefreshRequested = new(); + public static readonly EventChannel TestServerRequested = new(); + public static readonly EventChannel InboundDisplayRequested = new(); + public static readonly EventChannel SysProxyChangeRequested = new(); +} diff --git a/v2rayN/ServiceLib/Events/EventChannel.cs b/v2rayN/ServiceLib/Events/EventChannel.cs new file mode 100644 index 00000000..4ca040c6 --- /dev/null +++ b/v2rayN/ServiceLib/Events/EventChannel.cs @@ -0,0 +1,27 @@ +using System.Reactive.Subjects; + +namespace ServiceLib.Events; + +public sealed class EventChannel +{ + private readonly ISubject _subject = Subject.Synchronize(new Subject()); + + public IObservable AsObservable() + { + return _subject.AsObservable(); + } + + public void Publish(T value) + { + _subject.OnNext(value); + } + + public void Publish() + { + if (typeof(T) != typeof(Unit)) + { + throw new InvalidOperationException("Publish() without value is only valid for EventChannel."); + } + _subject.OnNext((T)(object)Unit.Default); + } +} diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index a02b81bb..88738d6d 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -50,6 +50,7 @@ public class Global public const string DirectTag = "direct"; public const string BlockTag = "block"; public const string DnsTag = "dns-module"; + public const string BalancerTagSuffix = "-round"; public const string StreamSecurity = "tls"; public const string StreamSecurityReality = "reality"; public const string Loopback = "127.0.0.1"; @@ -83,8 +84,7 @@ public class Global public const string SingboxDirectDNSTag = "direct_dns"; public const string SingboxRemoteDNSTag = "remote_dns"; - public const string SingboxOutboundResolverTag = "outbound_resolver"; - public const string SingboxFinalResolverTag = "final_resolver"; + public const string SingboxLocalDNSTag = "local_local"; public const string SingboxHostsDNSTag = "hosts_dns"; public const string SingboxFakeDNSTag = "fake_dns"; @@ -315,6 +315,8 @@ public class Global EConfigType.HTTP, ]; + public static readonly HashSet SingboxOnlyConfigType = SingboxSupportConfigType.Except(XraySupportConfigType).ToHashSet(); + public static readonly List DomainStrategies = [ AsIs, @@ -425,6 +427,7 @@ public class Global "zh-Hant", "en", "fa-Ir", + "fr", "ru", "hu" ]; @@ -449,6 +452,14 @@ public class Global "none" ]; + public static readonly Dictionary LogLevelColors = new() + { + { "debug", "#6C757D" }, + { "info", "#2ECC71" }, + { "warning", "#FFA500" }, + { "error", "#E74C3C" }, + }; + public static readonly List InboundTags = [ "socks", @@ -598,6 +609,7 @@ public class Global { "cloudflare-dns.com", new List { "104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9" } }, { "dns.cloudflare.com", new List { "104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5" } }, { "dot.pub", new List { "1.12.12.12", "120.53.53.53" } }, + { "doh.pub", new List { "1.12.12.12", "120.53.53.53" } }, { "dns.quad9.net", new List { "9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9" } }, { "dns.yandex.net", new List { "77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff" } }, { "dns.sb", new List { "185.222.222.222", "2a09::" } }, diff --git a/v2rayN/ServiceLib/GlobalUsings.cs b/v2rayN/ServiceLib/GlobalUsings.cs index 9a78c73b..d38ccc06 100644 --- a/v2rayN/ServiceLib/GlobalUsings.cs +++ b/v2rayN/ServiceLib/GlobalUsings.cs @@ -1,13 +1,36 @@ +global using System.Collections.Concurrent; +global using System.Diagnostics; +global using System.Net; +global using System.Net.NetworkInformation; +global using System.Net.Sockets; +global using System.Reactive; +global using System.Reactive.Disposables; +global using System.Reactive.Linq; +global using System.Reflection; +global using System.Runtime.InteropServices; +global using System.Security.Cryptography; +global using System.Text; +global using System.Text.Encodings.Web; +global using System.Text.Json; +global using System.Text.Json.Nodes; +global using System.Text.Json.Serialization; +global using System.Text.RegularExpressions; +global using DynamicData; +global using DynamicData.Binding; +global using ReactiveUI; +global using ReactiveUI.Fody.Helpers; global using ServiceLib.Base; global using ServiceLib.Common; global using ServiceLib.Enums; +global using ServiceLib.Events; global using ServiceLib.Handler; +global using ServiceLib.Handler.Fmt; +global using ServiceLib.Handler.SysProxy; global using ServiceLib.Helper; global using ServiceLib.Manager; -global using ServiceLib.Handler.Fmt; -global using ServiceLib.Services; -global using ServiceLib.Services.Statistics; -global using ServiceLib.Services.CoreConfig; global using ServiceLib.Models; global using ServiceLib.Resx; -global using ServiceLib.Handler.SysProxy; +global using ServiceLib.Services; +global using ServiceLib.Services.CoreConfig; +global using ServiceLib.Services.Statistics; +global using SQLite; diff --git a/v2rayN/ServiceLib/Handler/AppEvents.cs b/v2rayN/ServiceLib/Handler/AppEvents.cs deleted file mode 100644 index 109ee762..00000000 --- a/v2rayN/ServiceLib/Handler/AppEvents.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Reactive; -using System.Reactive.Subjects; - -namespace ServiceLib.Handler; - -public static class AppEvents -{ - public static readonly Subject ProfilesRefreshRequested = new(); - - public static readonly Subject SendSnackMsgRequested = new(); - - public static readonly Subject SendMsgViewRequested = new(); - - public static readonly Subject AppExitRequested = new(); - - public static readonly Subject ShutdownRequested = new(); - - public static readonly Subject AdjustMainLvColWidthRequested = new(); - - public static readonly Subject DispatcherStatisticsRequested = new(); -} diff --git a/v2rayN/ServiceLib/Handler/AutoStartupHandler.cs b/v2rayN/ServiceLib/Handler/AutoStartupHandler.cs index 015271d9..bc86cd7f 100644 --- a/v2rayN/ServiceLib/Handler/AutoStartupHandler.cs +++ b/v2rayN/ServiceLib/Handler/AutoStartupHandler.cs @@ -1,5 +1,4 @@ using System.Security.Principal; -using System.Text.RegularExpressions; namespace ServiceLib.Handler; @@ -27,7 +26,7 @@ public static class AutoStartupHandler await SetTaskLinux(); } } - else if (Utils.IsOSX()) + else if (Utils.IsMacOS()) { await ClearTaskOSX(); diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index 786f3aff..b3433437 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -1,5 +1,4 @@ using System.Data; -using System.Text.RegularExpressions; namespace ServiceLib.Handler; @@ -98,7 +97,7 @@ public static class ConfigHandler config.UiItem ??= new UIItem() { - EnableAutoAdjustMainLvColWidth = true + EnableUpdateSubOnlyRemarksExist = true }; config.UiItem.MainColumnItem ??= new(); config.UiItem.WindowSizeItem ??= new(); @@ -113,10 +112,8 @@ public static class ConfigHandler config.ConstItem ??= new ConstItem(); config.SimpleDNSItem ??= InitBuiltinSimpleDNS(); - if (config.SimpleDNSItem.GlobalFakeIp is null) - { - config.SimpleDNSItem.GlobalFakeIp = true; - } + config.SimpleDNSItem.GlobalFakeIp ??= true; + config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault(); config.SpeedTestItem ??= new(); if (config.SpeedTestItem.SpeedTestTimeout < 10) @@ -255,6 +252,7 @@ public static class ConfigHandler item.Mldsa65Verify = profileItem.Mldsa65Verify; item.Extra = profileItem.Extra; item.MuxEnabled = profileItem.MuxEnabled; + item.Cert = profileItem.Cert; } var ret = item.ConfigType switch @@ -357,6 +355,11 @@ public static class ConfigHandler { } } + else if (profileItem.ConfigType.IsGroupType()) + { + var profileGroupItem = await AppManager.Instance.GetProfileGroupItem(it.IndexId); + await AddGroupServerCommon(config, profileItem, profileGroupItem, true); + } else { await AddServerCommon(config, profileItem, true); @@ -445,13 +448,13 @@ public static class ConfigHandler /// 0 if successful, -1 if failed public static async Task MoveServer(Config config, List lstProfile, int index, EMove eMove, int pos = -1) { - int count = lstProfile.Count; + var count = lstProfile.Count; if (index < 0 || index > lstProfile.Count - 1) { return -1; } - for (int i = 0; i < lstProfile.Count; i++) + for (var i = 0; i < lstProfile.Count; i++) { ProfileExManager.Instance.SetSort(lstProfile[i].IndexId, (i + 1) * 10); } @@ -525,7 +528,7 @@ public static class ConfigHandler return -1; } var ext = Path.GetExtension(fileName); - string newFileName = $"{Utils.GetGuid()}{ext}"; + var newFileName = $"{Utils.GetGuid()}{ext}"; //newFileName = Path.Combine(Utile.GetTempPath(), newFileName); try @@ -1074,6 +1077,37 @@ public static class ConfigHandler return 0; } + public static async Task AddGroupServerCommon(Config config, ProfileItem profileItem, ProfileGroupItem profileGroupItem, bool toFile = true) + { + var maxSort = -1; + if (profileItem.IndexId.IsNullOrEmpty()) + { + profileItem.IndexId = Utils.GetGuid(false); + maxSort = ProfileExManager.Instance.GetMaxSort(); + } + var groupType = profileItem.ConfigType == EConfigType.ProxyChain ? EConfigType.ProxyChain.ToString() : profileGroupItem.MultipleLoad.ToString(); + profileItem.Address = $"{profileItem.CoreType}-{groupType}"; + if (maxSort > 0) + { + ProfileExManager.Instance.SetSort(profileItem.IndexId, maxSort + 1); + } + if (toFile) + { + await SQLiteHelper.Instance.ReplaceAsync(profileItem); + if (profileGroupItem != null) + { + profileGroupItem.IndexId = profileItem.IndexId; + await ProfileGroupItemManager.Instance.SaveItemAsync(profileGroupItem); + } + else + { + ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(profileItem.IndexId); + await ProfileGroupItemManager.Instance.SaveTo(); + } + } + return 0; + } + /// /// Compare two profile items to determine if they represent the same server /// Used for deduplication and server matching @@ -1145,7 +1179,7 @@ public static class ConfigHandler } /// - /// Create a custom server that combines multiple servers for load balancing + /// Create a group server that combines multiple servers for load balancing /// Generates a configuration file that references multiple servers /// /// Current configuration @@ -1153,45 +1187,55 @@ public static class ConfigHandler /// Core type to use (Xray or sing_box) /// Load balancing algorithm /// Result object with success state and data - public static async Task AddCustomServer4Multiple(Config config, List selecteds, ECoreType coreType, EMultipleLoad multipleLoad) + public static async Task AddGroupServer4Multiple(Config config, List selecteds, ECoreType coreType, EMultipleLoad multipleLoad, string? subId) { - var indexId = Utils.GetMd5(Global.CoreMultipleLoadConfigFileName); - var configPath = Utils.GetConfigPath(Global.CoreMultipleLoadConfigFileName); + var result = new RetResult(); - var result = await CoreConfigHandler.GenerateClientMultipleLoadConfig(config, configPath, selecteds, coreType, multipleLoad); - if (result.Success != true) - { - return result; - } + var indexId = Utils.GetGuid(false); + var childProfileIndexId = Utils.List2String(selecteds.Select(p => p.IndexId).ToList()); - if (!File.Exists(configPath)) - { - return result; - } - - var profileItem = await AppManager.Instance.GetProfileItem(indexId) ?? new(); - profileItem.IndexId = indexId; + var remark = subId.IsNullOrEmpty() ? string.Empty : $"{(await AppManager.Instance.GetSubItem(subId)).Remarks} "; if (coreType == ECoreType.Xray) { - profileItem.Remarks = multipleLoad switch + remark += multipleLoad switch { - EMultipleLoad.Random => ResUI.menuSetDefaultMultipleServerXrayRandom, - EMultipleLoad.RoundRobin => ResUI.menuSetDefaultMultipleServerXrayRoundRobin, - EMultipleLoad.LeastPing => ResUI.menuSetDefaultMultipleServerXrayLeastPing, - EMultipleLoad.LeastLoad => ResUI.menuSetDefaultMultipleServerXrayLeastLoad, - _ => ResUI.menuSetDefaultMultipleServerXrayRoundRobin, + EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerXrayLeastPing, + EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerXrayFallback, + EMultipleLoad.Random => ResUI.menuGenGroupMultipleServerXrayRandom, + EMultipleLoad.RoundRobin => ResUI.menuGenGroupMultipleServerXrayRoundRobin, + EMultipleLoad.LeastLoad => ResUI.menuGenGroupMultipleServerXrayLeastLoad, + _ => ResUI.menuGenGroupMultipleServerXrayRoundRobin, }; } else if (coreType == ECoreType.sing_box) { - profileItem.Remarks = ResUI.menuSetDefaultMultipleServerSingBoxLeastPing; + remark += multipleLoad switch + { + EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerSingBoxLeastPing, + EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerSingBoxFallback, + _ => ResUI.menuGenGroupMultipleServerSingBoxLeastPing, + }; } - profileItem.Address = Global.CoreMultipleLoadConfigFileName; - profileItem.ConfigType = EConfigType.Custom; - profileItem.CoreType = coreType; - - await AddServerCommon(config, profileItem, true); - + var profile = new ProfileItem + { + IndexId = indexId, + CoreType = coreType, + ConfigType = EConfigType.PolicyGroup, + Remarks = remark, + IsSub = false + }; + if (!subId.IsNullOrEmpty()) + { + profile.Subid = subId; + } + var profileGroup = new ProfileGroupItem + { + ChildItems = childProfileIndexId, + MultipleLoad = multipleLoad, + IndexId = indexId, + }; + var ret = await AddGroupServerCommon(config, profile, profileGroup, true); + result.Success = ret == 0; result.Data = indexId; return result; } @@ -1209,16 +1253,25 @@ public static class ConfigHandler ProfileItem? itemSocks = null; if (node.ConfigType != EConfigType.Custom && coreType != ECoreType.sing_box && config.TunModeItem.EnableTun) { + var tun2SocksAddress = node.Address; + if (node.ConfigType.IsGroupType()) + { + var lstAddresses = (await ProfileGroupItemManager.GetAllChildDomainAddresses(node.IndexId)).ToList(); + if (lstAddresses.Count > 0) + { + tun2SocksAddress = Utils.List2String(lstAddresses); + } + } itemSocks = new ProfileItem() { CoreType = ECoreType.sing_box, ConfigType = EConfigType.SOCKS, Address = Global.Loopback, - Sni = node.Address, //Tun2SocksAddress + SpiderX = tun2SocksAddress, // Tun2SocksAddress Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks) }; } - else if ((node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0)) + else if (node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0) { var preCoreType = config.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray; itemSocks = new ProfileItem() @@ -1304,7 +1357,7 @@ public static class ConfigHandler } continue; } - var profileItem = FmtHandler.ResolveConfig(str, out string msg); + var profileItem = FmtHandler.ResolveConfig(str, out var msg); if (profileItem is null) { continue; @@ -1388,7 +1441,7 @@ public static class ConfigHandler { await RemoveServersViaSubid(config, subid, isSub); } - int count = 0; + var count = 0; foreach (var it in lstProfiles) { it.Subid = subid; @@ -1431,15 +1484,6 @@ public static class ConfigHandler { profileItem = Hysteria2Fmt.ResolveFull2(strData, subRemarks); } - if (profileItem is null) - { - profileItem = Hysteria2Fmt.ResolveFull(strData, subRemarks); - } - //Is naiveproxy configuration - if (profileItem is null) - { - profileItem = NaiveproxyFmt.ResolveFull(strData, subRemarks); - } if (profileItem is null || profileItem.Address.IsNullOrEmpty()) { return -1; @@ -1487,7 +1531,7 @@ public static class ConfigHandler var lstSsServer = ShadowsocksFmt.ResolveSip008(strData); if (lstSsServer?.Count > 0) { - int counter = 0; + var counter = 0; foreach (var ssItem in lstSsServer) { ssItem.Subid = subid; @@ -1607,7 +1651,9 @@ public static class ConfigHandler var uri = Utils.TryUri(url); if (uri == null) + { return -1; + } //Do not allow http protocol if (url.StartsWith(Global.HttpProtocol) && !Utils.IsPrivateNetwork(uri.IdnHost)) { @@ -1662,7 +1708,7 @@ public static class ConfigHandler var maxSort = 0; if (await SQLiteHelper.Instance.TableAsync().CountAsync() > 0) { - var lstSubs = (await AppManager.Instance.SubItems()); + var lstSubs = await AppManager.Instance.SubItems(); maxSort = lstSubs.LastOrDefault()?.Sort ?? 0; } item.Sort = maxSort + 1; @@ -1824,7 +1870,7 @@ public static class ConfigHandler /// 0 if successful, -1 if failed public static async Task MoveRoutingRule(List rules, int index, EMove eMove, int pos = -1) { - int count = rules.Count; + var count = rules.Count; if (index < 0 || index > rules.Count - 1) { return -1; @@ -1974,11 +2020,15 @@ public static class ConfigHandler var downloadHandle = new DownloadService(); var templateContent = await downloadHandle.TryDownloadString(config.ConstItem.RouteRulesTemplateSourceUrl, true, ""); if (templateContent.IsNullOrEmpty()) + { return await InitBuiltinRouting(config, blImportAdvancedRules); // fallback + } var template = JsonUtils.Deserialize(templateContent); if (template == null) + { return await InitBuiltinRouting(config, blImportAdvancedRules); // fallback + } var items = await AppManager.Instance.RoutingItems(); var maxSort = items.Count; @@ -1991,14 +2041,18 @@ public static class ConfigHandler var item = template.RoutingItems[i]; if (item.Url.IsNullOrEmpty() && item.RuleSet.IsNullOrEmpty()) + { continue; + } var ruleSetsString = !item.RuleSet.IsNullOrEmpty() ? item.RuleSet : await downloadHandle.TryDownloadString(item.Url, true, ""); if (ruleSetsString.IsNullOrEmpty()) + { continue; + } item.Remarks = $"{template.Version}-{item.Remarks}"; item.Enabled = true; @@ -2194,17 +2248,25 @@ public static class ConfigHandler var downloadHandle = new DownloadService(); var templateContent = await downloadHandle.TryDownloadString(url, true, ""); if (templateContent.IsNullOrEmpty()) + { return currentItem; + } var template = JsonUtils.Deserialize(templateContent); if (template == null) + { return currentItem; + } if (!template.NormalDNS.IsNullOrEmpty()) + { template.NormalDNS = await downloadHandle.TryDownloadString(template.NormalDNS, true, ""); + } if (!template.TunDNS.IsNullOrEmpty()) + { template.TunDNS = await downloadHandle.TryDownloadString(template.TunDNS, true, ""); + } template.Id = currentItem.Id; template.Enabled = currentItem.Enabled; @@ -2229,8 +2291,7 @@ public static class ConfigHandler BlockBindingQuery = true, DirectDNS = Global.DomainDirectDNSAddress.FirstOrDefault(), RemoteDNS = Global.DomainRemoteDNSAddress.FirstOrDefault(), - SingboxOutboundsResolveDNS = Global.DomainDirectDNSAddress.FirstOrDefault(), - SingboxFinalResolveDNS = Global.DomainPureIPDNSAddress.FirstOrDefault() + BootstrapDNS = Global.DomainPureIPDNSAddress.FirstOrDefault(), }; } @@ -2239,10 +2300,16 @@ public static class ConfigHandler var downloadHandle = new DownloadService(); var templateContent = await downloadHandle.TryDownloadString(url, true, ""); if (templateContent.IsNullOrEmpty()) + { return null; + } + var template = JsonUtils.Deserialize(templateContent); if (template == null) + { return null; + } + return template; } diff --git a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs index 38f3ae51..05825d93 100644 --- a/v2rayN/ServiceLib/Handler/ConnectionHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConnectionHandler.cs @@ -1,5 +1,3 @@ -using System.Net; - namespace ServiceLib.Handler; public static class ConnectionHandler @@ -8,7 +6,7 @@ public static class ConnectionHandler public static async Task RunAvailabilityCheck() { - var time = await GetRealPingTime(); + var time = await GetRealPingTimeInfo(); var ip = time > 0 ? await GetIPInfo() ?? Global.None : Global.None; return string.Format(ResUI.TestMeOutput, time, ip); @@ -41,7 +39,7 @@ public static class ConnectionHandler return $"({country ?? "unknown"}) {ip}"; } - private static async Task GetRealPingTime() + private static async Task GetRealPingTimeInfo() { var responseTime = -1; try @@ -52,7 +50,7 @@ public static class ConnectionHandler for (var i = 0; i < 2; i++) { - responseTime = await HttpClientHelper.Instance.GetRealPingTime(url, webProxy, 10); + responseTime = await GetRealPingTime(url, webProxy, 10); if (responseTime > 0) { break; @@ -67,4 +65,34 @@ public static class ConnectionHandler } return responseTime; } + + public static async Task GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout) + { + var responseTime = -1; + try + { + using var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout)); + using var client = new HttpClient(new SocketsHttpHandler() + { + Proxy = webProxy, + UseProxy = webProxy != null + }); + + List oneTime = new(); + for (var i = 0; i < 2; i++) + { + var timer = Stopwatch.StartNew(); + await client.GetAsync(url, cts.Token).ConfigureAwait(false); + timer.Stop(); + oneTime.Add((int)timer.Elapsed.TotalMilliseconds); + await Task.Delay(100); + } + responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault(); + } + catch + { + } + return responseTime; + } } diff --git a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs index 66d261ac..f51e2051 100644 --- a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs @@ -58,7 +58,7 @@ public static class CoreConfigHandler File.Delete(fileName); } - string addressFileName = node.Address; + var addressFileName = node.Address; if (!File.Exists(addressFileName)) { addressFileName = Utils.GetConfigPath(addressFileName); @@ -132,24 +132,4 @@ public static class CoreConfigHandler await File.WriteAllTextAsync(fileName, result.Data.ToString()); return result; } - - public static async Task GenerateClientMultipleLoadConfig(Config config, string fileName, List selecteds, ECoreType coreType, EMultipleLoad multipleLoad) - { - var result = new RetResult(); - if (coreType == ECoreType.sing_box) - { - result = await new CoreConfigSingboxService(config).GenerateClientMultipleLoadConfig(selecteds); - } - else - { - result = await new CoreConfigV2rayService(config).GenerateClientMultipleLoadConfig(selecteds, multipleLoad); - } - - if (result.Success != true) - { - return result; - } - await File.WriteAllTextAsync(fileName, result.Data.ToString()); - return result; - } } diff --git a/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs index f175ce82..f098b6a4 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs @@ -23,7 +23,7 @@ public class AnytlsFmt : BaseFmt item.Id = rawUserInfo; var query = Utils.ParseQueryString(parsedUrl.Query); - _ = ResolveStdTransport(query, ref item); + ResolveUriQuery(query, ref item); return item; } @@ -41,7 +41,7 @@ public class AnytlsFmt : BaseFmt } var pw = item.Id; var dicQuery = new Dictionary(); - _ = GetStdTransport(item, Global.None, ref dicQuery); + ToUriQuery(item, Global.None, ref dicQuery); return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark); } diff --git a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs index 965efbb1..481e183b 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs @@ -4,6 +4,8 @@ namespace ServiceLib.Handler.Fmt; public class BaseFmt { + private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure", "verify" }; + protected static string GetIpv6(string address) { if (Utils.IsIpv6(address)) @@ -17,7 +19,7 @@ public class BaseFmt } } - protected static int GetStdTransport(ProfileItem item, string? securityDef, ref Dictionary dicQuery) + protected static int ToUriQuery(ProfileItem item, string? securityDef, ref Dictionary dicQuery) { if (item.Flow.IsNotEmpty()) { @@ -37,11 +39,7 @@ public class BaseFmt } if (item.Sni.IsNotEmpty()) { - dicQuery.Add("sni", item.Sni); - } - if (item.Alpn.IsNotEmpty()) - { - dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); + dicQuery.Add("sni", Utils.UrlEncode(item.Sni)); } if (item.Fingerprint.IsNotEmpty()) { @@ -63,9 +61,14 @@ public class BaseFmt { dicQuery.Add("pqv", Utils.UrlEncode(item.Mldsa65Verify)); } - if (item.AllowInsecure.Equals("true")) + + if (item.StreamSecurity.Equals(Global.StreamSecurity)) { - dicQuery.Add("allowInsecure", "1"); + if (item.Alpn.IsNotEmpty()) + { + dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); + } + ToUriQueryAllowInsecure(item, ref dicQuery); } dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp)); @@ -153,63 +156,107 @@ public class BaseFmt return 0; } - protected static int ResolveStdTransport(NameValueCollection query, ref ProfileItem item) + protected static int ToUriQueryLite(ProfileItem item, ref Dictionary dicQuery) { - item.Flow = query["flow"] ?? ""; - item.StreamSecurity = query["security"] ?? ""; - item.Sni = query["sni"] ?? ""; - item.Alpn = Utils.UrlDecode(query["alpn"] ?? ""); - item.Fingerprint = Utils.UrlDecode(query["fp"] ?? ""); - item.PublicKey = Utils.UrlDecode(query["pbk"] ?? ""); - item.ShortId = Utils.UrlDecode(query["sid"] ?? ""); - item.SpiderX = Utils.UrlDecode(query["spx"] ?? ""); - item.Mldsa65Verify = Utils.UrlDecode(query["pqv"] ?? ""); - item.AllowInsecure = (query["allowInsecure"] ?? "") == "1" ? "true" : ""; + if (item.Sni.IsNotEmpty()) + { + dicQuery.Add("sni", Utils.UrlEncode(item.Sni)); + } + if (item.Alpn.IsNotEmpty()) + { + dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); + } - item.Network = query["type"] ?? nameof(ETransport.tcp); + ToUriQueryAllowInsecure(item, ref dicQuery); + + return 0; + } + + private static int ToUriQueryAllowInsecure(ProfileItem item, ref Dictionary dicQuery) + { + if (item.AllowInsecure.Equals(Global.AllowInsecure.First())) + { + // Add two for compatibility + dicQuery.Add("insecure", "1"); + dicQuery.Add("allowInsecure", "1"); + } + else + { + dicQuery.Add("insecure", "0"); + dicQuery.Add("allowInsecure", "0"); + } + + return 0; + } + + protected static int ResolveUriQuery(NameValueCollection query, ref ProfileItem item) + { + item.Flow = GetQueryValue(query, "flow"); + item.StreamSecurity = GetQueryValue(query, "security"); + item.Sni = GetQueryValue(query, "sni"); + item.Alpn = GetQueryDecoded(query, "alpn"); + item.Fingerprint = GetQueryDecoded(query, "fp"); + item.PublicKey = GetQueryDecoded(query, "pbk"); + item.ShortId = GetQueryDecoded(query, "sid"); + item.SpiderX = GetQueryDecoded(query, "spx"); + item.Mldsa65Verify = GetQueryDecoded(query, "pqv"); + + if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1")) + { + item.AllowInsecure = Global.AllowInsecure.First(); + } + else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0")) + { + item.AllowInsecure = Global.AllowInsecure.Skip(1).First(); + } + else + { + item.AllowInsecure = string.Empty; + } + + item.Network = GetQueryValue(query, "type", nameof(ETransport.tcp)); switch (item.Network) { case nameof(ETransport.tcp): - item.HeaderType = query["headerType"] ?? Global.None; - item.RequestHost = Utils.UrlDecode(query["host"] ?? ""); - + item.HeaderType = GetQueryValue(query, "headerType", Global.None); + item.RequestHost = GetQueryDecoded(query, "host"); break; case nameof(ETransport.kcp): - item.HeaderType = query["headerType"] ?? Global.None; - item.Path = Utils.UrlDecode(query["seed"] ?? ""); + item.HeaderType = GetQueryValue(query, "headerType", Global.None); + item.Path = GetQueryDecoded(query, "seed"); break; case nameof(ETransport.ws): case nameof(ETransport.httpupgrade): - item.RequestHost = Utils.UrlDecode(query["host"] ?? ""); - item.Path = Utils.UrlDecode(query["path"] ?? "/"); + item.RequestHost = GetQueryDecoded(query, "host"); + item.Path = GetQueryDecoded(query, "path", "/"); break; case nameof(ETransport.xhttp): - item.RequestHost = Utils.UrlDecode(query["host"] ?? ""); - item.Path = Utils.UrlDecode(query["path"] ?? "/"); - item.HeaderType = Utils.UrlDecode(query["mode"] ?? ""); - item.Extra = Utils.UrlDecode(query["extra"] ?? ""); + item.RequestHost = GetQueryDecoded(query, "host"); + item.Path = GetQueryDecoded(query, "path", "/"); + item.HeaderType = GetQueryDecoded(query, "mode"); + item.Extra = GetQueryDecoded(query, "extra"); break; case nameof(ETransport.http): case nameof(ETransport.h2): item.Network = nameof(ETransport.h2); - item.RequestHost = Utils.UrlDecode(query["host"] ?? ""); - item.Path = Utils.UrlDecode(query["path"] ?? "/"); + item.RequestHost = GetQueryDecoded(query, "host"); + item.Path = GetQueryDecoded(query, "path", "/"); break; case nameof(ETransport.quic): - item.HeaderType = query["headerType"] ?? Global.None; - item.RequestHost = query["quicSecurity"] ?? Global.None; - item.Path = Utils.UrlDecode(query["key"] ?? ""); + item.HeaderType = GetQueryValue(query, "headerType", Global.None); + item.RequestHost = GetQueryValue(query, "quicSecurity", Global.None); + item.Path = GetQueryDecoded(query, "key"); break; case nameof(ETransport.grpc): - item.RequestHost = Utils.UrlDecode(query["authority"] ?? ""); - item.Path = Utils.UrlDecode(query["serviceName"] ?? ""); - item.HeaderType = Utils.UrlDecode(query["mode"] ?? Global.GrpcGunMode); + item.RequestHost = GetQueryDecoded(query, "authority"); + item.Path = GetQueryDecoded(query, "serviceName"); + item.HeaderType = GetQueryDecoded(query, "mode", Global.GrpcGunMode); break; default: @@ -239,4 +286,14 @@ public class BaseFmt var url = $"{Utils.UrlEncode(userInfo)}@{GetIpv6(address)}:{port}"; return $"{Global.ProtocolShares[eConfigType]}{url}{query}{remark}"; } + + protected static string GetQueryValue(NameValueCollection query, string key, string defaultValue = "") + { + return query[key] ?? defaultValue; + } + + protected static string GetQueryDecoded(NameValueCollection query, string key, string defaultValue = "") + { + return Utils.UrlDecode(GetQueryValue(query, key, defaultValue)); + } } diff --git a/v2rayN/ServiceLib/Handler/Fmt/ClashFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/ClashFmt.cs index 6c6f2bbf..5925e0cb 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/ClashFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/ClashFmt.cs @@ -4,7 +4,7 @@ public class ClashFmt : BaseFmt { public static ProfileItem? ResolveFull(string strData, string? subRemarks) { - if (Contains(strData, "external-controller", "-port", "proxies")) + if (Contains(strData, "rules", "-port", "proxies")) { var fileName = WriteAllText(strData, "yaml"); diff --git a/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs b/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs index 814d753d..4fc251b7 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/FmtHandler.cs @@ -27,7 +27,7 @@ public class FmtHandler catch (Exception ex) { Logging.SaveLog(_tag, ex); - return ""; + return string.Empty; } } @@ -37,7 +37,7 @@ public class FmtHandler try { - string str = config.TrimEx(); + var str = config.TrimEx(); if (str.IsNullOrEmpty()) { msg = ResUI.FailedReadConfiguration; diff --git a/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs b/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs index bc56a302..c8d6a71a 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs @@ -12,7 +12,9 @@ public class Hysteria2Fmt : BaseFmt var url = Utils.TryUri(str); if (url == null) + { return null; + } item.Address = url.IdnHost; item.Port = url.Port; @@ -20,11 +22,9 @@ public class Hysteria2Fmt : BaseFmt item.Id = Utils.UrlDecode(url.UserInfo); var query = Utils.ParseQueryString(url.Query); - ResolveStdTransport(query, ref item); - item.Path = Utils.UrlDecode(query["obfs-password"] ?? ""); - item.AllowInsecure = (query["insecure"] ?? "") == "1" ? "true" : "false"; - - item.Ports = Utils.UrlDecode(query["mport"] ?? ""); + ResolveUriQuery(query, ref item); + item.Path = GetQueryDecoded(query, "obfs-password"); + item.Ports = GetQueryDecoded(query, "mport"); return item; } @@ -32,29 +32,25 @@ public class Hysteria2Fmt : BaseFmt public static string? ToUri(ProfileItem? item) { if (item == null) + { return null; - string url = string.Empty; + } - string remark = string.Empty; + var url = string.Empty; + + var remark = string.Empty; if (item.Remarks.IsNotEmpty()) { remark = "#" + Utils.UrlEncode(item.Remarks); } var dicQuery = new Dictionary(); - if (item.Sni.IsNotEmpty()) - { - dicQuery.Add("sni", item.Sni); - } - if (item.Alpn.IsNotEmpty()) - { - dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); - } + ToUriQueryLite(item, ref dicQuery); + if (item.Path.IsNotEmpty()) { dicQuery.Add("obfs", "salamander"); dicQuery.Add("obfs-password", Utils.UrlEncode(item.Path)); } - dicQuery.Add("insecure", item.AllowInsecure.ToLower() == "true" ? "1" : "0"); if (item.Ports.IsNotEmpty()) { dicQuery.Add("mport", Utils.UrlEncode(item.Ports.Replace(':', '-'))); @@ -63,24 +59,6 @@ public class Hysteria2Fmt : BaseFmt return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Id, dicQuery, remark); } - public static ProfileItem? ResolveFull(string strData, string? subRemarks) - { - if (Contains(strData, "server", "up", "down", "listen", "", "")) - { - var fileName = WriteAllText(strData); - - var profileItem = new ProfileItem - { - CoreType = ECoreType.hysteria, - Address = fileName, - Remarks = subRemarks ?? "hysteria_custom" - }; - return profileItem; - } - - return null; - } - public static ProfileItem? ResolveFull2(string strData, string? subRemarks) { if (Contains(strData, "server", "auth", "up", "down", "listen")) diff --git a/v2rayN/ServiceLib/Handler/Fmt/NaiveproxyFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/NaiveproxyFmt.cs deleted file mode 100644 index 58f34d1f..00000000 --- a/v2rayN/ServiceLib/Handler/Fmt/NaiveproxyFmt.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace ServiceLib.Handler.Fmt; - -public class NaiveproxyFmt : BaseFmt -{ - public static ProfileItem? ResolveFull(string strData, string? subRemarks) - { - if (Contains(strData, "listen", "proxy", "", "")) - { - var fileName = WriteAllText(strData); - - var profileItem = new ProfileItem - { - CoreType = ECoreType.naiveproxy, - Address = fileName, - Remarks = subRemarks ?? "naiveproxy_custom" - }; - return profileItem; - } - - return null; - } -} diff --git a/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs index 2c9898e9..adaeb954 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs @@ -1,5 +1,3 @@ -using System.Text.RegularExpressions; - namespace ServiceLib.Handler.Fmt; public class ShadowsocksFmt : BaseFmt @@ -42,7 +40,7 @@ public class ShadowsocksFmt : BaseFmt // item.port); //url = Utile.Base64Encode(url); //new Sip002 - var pw = Utils.Base64Encode($"{item.Security}:{item.Id}"); + var pw = Utils.Base64Encode($"{item.Security}:{item.Id}", true); return ToUri(EConfigType.Shadowsocks, item.Address, item.Port, pw, null, remark); } diff --git a/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs index 6110d784..dbecdade 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs @@ -33,7 +33,7 @@ public class SocksFmt : BaseFmt remark = "#" + Utils.UrlEncode(item.Remarks); } //new - var pw = Utils.Base64Encode($"{item.Security}:{item.Id}"); + var pw = Utils.Base64Encode($"{item.Security}:{item.Id}", true); return ToUri(EConfigType.SOCKS, item.Address, item.Port, pw, null, remark); } diff --git a/v2rayN/ServiceLib/Handler/Fmt/TrojanFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/TrojanFmt.cs index 3a4ee984..dc4794d8 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/TrojanFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/TrojanFmt.cs @@ -23,7 +23,7 @@ public class TrojanFmt : BaseFmt item.Id = Utils.UrlDecode(url.UserInfo); var query = Utils.ParseQueryString(url.Query); - _ = ResolveStdTransport(query, ref item); + ResolveUriQuery(query, ref item); return item; } @@ -40,7 +40,7 @@ public class TrojanFmt : BaseFmt remark = "#" + Utils.UrlEncode(item.Remarks); } var dicQuery = new Dictionary(); - _ = GetStdTransport(item, null, ref dicQuery); + ToUriQuery(item, null, ref dicQuery); return ToUri(EConfigType.Trojan, item.Address, item.Port, item.Id, dicQuery, remark); } diff --git a/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs index 632f49c9..1c5aded6 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs @@ -29,8 +29,8 @@ public class TuicFmt : BaseFmt } var query = Utils.ParseQueryString(url.Query); - ResolveStdTransport(query, ref item); - item.HeaderType = query["congestion_control"] ?? ""; + ResolveUriQuery(query, ref item); + item.HeaderType = GetQueryValue(query, "congestion_control"); return item; } @@ -47,15 +47,10 @@ public class TuicFmt : BaseFmt { remark = "#" + Utils.UrlEncode(item.Remarks); } + var dicQuery = new Dictionary(); - if (item.Sni.IsNotEmpty()) - { - dicQuery.Add("sni", item.Sni); - } - if (item.Alpn.IsNotEmpty()) - { - dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); - } + ToUriQueryLite(item, ref dicQuery); + dicQuery.Add("congestion_control", item.HeaderType); return ToUri(EConfigType.TUIC, item.Address, item.Port, $"{item.Id}:{item.Security}", dicQuery, remark); diff --git a/v2rayN/ServiceLib/Handler/Fmt/VLESSFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/VLESSFmt.cs index 4d3bcff0..3048b51c 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/VLESSFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/VLESSFmt.cs @@ -24,9 +24,9 @@ public class VLESSFmt : BaseFmt item.Id = Utils.UrlDecode(url.UserInfo); var query = Utils.ParseQueryString(url.Query); - item.Security = query["encryption"] ?? Global.None; - item.StreamSecurity = query["security"] ?? ""; - _ = ResolveStdTransport(query, ref item); + item.Security = GetQueryValue(query, "encryption", Global.None); + item.StreamSecurity = GetQueryValue(query, "security"); + ResolveUriQuery(query, ref item); return item; } @@ -52,7 +52,7 @@ public class VLESSFmt : BaseFmt { dicQuery.Add("encryption", Global.None); } - _ = GetStdTransport(item, Global.None, ref dicQuery); + ToUriQuery(item, Global.None, ref dicQuery); return ToUri(EConfigType.VLESS, item.Address, item.Port, item.Id, dicQuery, remark); } diff --git a/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs index 5892236f..e6535d6e 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs @@ -39,7 +39,8 @@ public class VmessFmt : BaseFmt tls = item.StreamSecurity, sni = item.Sni, alpn = item.Alpn, - fp = item.Fingerprint + fp = item.Fingerprint, + insecure = item.AllowInsecure.Equals(Global.AllowInsecure.First()) ? "1" : "0" }; var url = JsonUtils.Serialize(vmessQRCode); @@ -94,6 +95,7 @@ public class VmessFmt : BaseFmt item.Sni = Utils.ToString(vmessQRCode.sni); item.Alpn = Utils.ToString(vmessQRCode.alpn); item.Fingerprint = Utils.ToString(vmessQRCode.fp); + item.AllowInsecure = vmessQRCode.insecure == "1" ? Global.AllowInsecure.First() : string.Empty; return item; } @@ -118,7 +120,7 @@ public class VmessFmt : BaseFmt item.Id = Utils.UrlDecode(url.UserInfo); var query = Utils.ParseQueryString(url.Query); - ResolveStdTransport(query, ref item); + ResolveUriQuery(query, ref item); return item; } diff --git a/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs index 8e78f036..6ceb945d 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs @@ -24,10 +24,10 @@ public class WireguardFmt : BaseFmt var query = Utils.ParseQueryString(url.Query); - item.PublicKey = Utils.UrlDecode(query["publickey"] ?? ""); - item.Path = Utils.UrlDecode(query["reserved"] ?? ""); - item.RequestHost = Utils.UrlDecode(query["address"] ?? ""); - item.ShortId = Utils.UrlDecode(query["mtu"] ?? ""); + item.PublicKey = GetQueryDecoded(query, "publickey"); + item.Path = GetQueryDecoded(query, "reserved"); + item.RequestHost = GetQueryDecoded(query, "address"); + item.ShortId = GetQueryDecoded(query, "mtu"); return item; } diff --git a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs index 828c2102..4929c72e 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingLinux.cs @@ -18,7 +18,13 @@ public static class ProxySettingLinux private static async Task ExecCmd(List args) { - var fileName = await FileManager.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetLinuxShellFileName), false); + var customSystemProxyScriptPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyScriptPath; + var fileName = (customSystemProxyScriptPath.IsNotEmpty() && File.Exists(customSystemProxyScriptPath)) + ? customSystemProxyScriptPath + : await FileUtils.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetLinuxShellFileName), false); + + // TODO: temporarily notify which script is being used + NoticeManager.Instance.SendMessage(fileName); await Utils.GetCliWrapOutput(fileName, args); } diff --git a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs index 85d9b821..56fbe24d 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingOSX.cs @@ -23,7 +23,13 @@ public static class ProxySettingOSX private static async Task ExecCmd(List args) { - var fileName = await FileManager.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetOSXShellFileName), false); + var customSystemProxyScriptPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyScriptPath; + var fileName = (customSystemProxyScriptPath.IsNotEmpty() && File.Exists(customSystemProxyScriptPath)) + ? customSystemProxyScriptPath + : await FileUtils.CreateLinuxShellFile(_proxySetFileName, EmbedUtils.GetEmbedText(Global.ProxySetOSXShellFileName), false); + + // TODO: temporarily notify which script is being used + NoticeManager.Instance.SendMessage(fileName); await Utils.GetCliWrapOutput(fileName, args); } diff --git a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingWindows.cs b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingWindows.cs index 44b7e046..8dc2f335 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingWindows.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/ProxySettingWindows.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using static ServiceLib.Handler.SysProxy.ProxySettingWindows.InternetConnectionOption; namespace ServiceLib.Handler.SysProxy; diff --git a/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs b/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs index 38ea04ae..21453591 100644 --- a/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs +++ b/v2rayN/ServiceLib/Handler/SysProxy/SysProxyHandler.cs @@ -33,7 +33,7 @@ public static class SysProxyHandler await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions); break; - case ESysProxyType.ForcedChange when Utils.IsOSX(): + case ESysProxyType.ForcedChange when Utils.IsMacOS(): await ProxySettingOSX.SetProxy(Global.Loopback, port, exceptions); break; @@ -45,7 +45,7 @@ public static class SysProxyHandler await ProxySettingLinux.UnsetProxy(); break; - case ESysProxyType.ForcedClear when Utils.IsOSX(): + case ESysProxyType.ForcedClear when Utils.IsMacOS(): await ProxySettingOSX.UnsetProxy(); break; @@ -91,7 +91,7 @@ public static class SysProxyHandler private static async Task SetWindowsProxyPac(int port) { var portPac = AppManager.Instance.GetLocalPort(EInboundProtocol.pac); - await PacManager.Instance.StartAsync(Utils.GetConfigPath(), port, portPac); + await PacManager.Instance.StartAsync(port, portPac); var strProxy = $"{Global.HttpProtocol}{Global.Loopback}:{portPac}/pac?t={DateTime.Now.Ticks}"; ProxySettingWindows.SetProxy(strProxy, "", 4); } diff --git a/v2rayN/ServiceLib/Helper/DownloaderHelper.cs b/v2rayN/ServiceLib/Helper/DownloaderHelper.cs index 3764499c..000685a0 100644 --- a/v2rayN/ServiceLib/Helper/DownloaderHelper.cs +++ b/v2rayN/ServiceLib/Helper/DownloaderHelper.cs @@ -1,4 +1,3 @@ -using System.Net; using Downloader; namespace ServiceLib.Helper; diff --git a/v2rayN/ServiceLib/Helper/HttpClientHelper.cs b/v2rayN/ServiceLib/Helper/HttpClientHelper.cs index a559800f..9bc38c01 100644 --- a/v2rayN/ServiceLib/Helper/HttpClientHelper.cs +++ b/v2rayN/ServiceLib/Helper/HttpClientHelper.cs @@ -1,8 +1,5 @@ -using System.Diagnostics; -using System.Net; using System.Net.Http.Headers; using System.Net.Mime; -using System.Text; namespace ServiceLib.Helper; @@ -52,15 +49,6 @@ public class HttpClientHelper return await httpClient.GetStringAsync(url); } - public async Task GetAsync(HttpClient client, string url, CancellationToken token = default) - { - if (url.IsNullOrEmpty()) - { - return null; - } - return await client.GetStringAsync(url, token); - } - public async Task PutAsync(string url, Dictionary headers) { var jsonContent = JsonUtils.Serialize(headers); @@ -83,156 +71,4 @@ public class HttpClientHelper { await httpClient.DeleteAsync(url); } - - public static async Task DownloadFileAsync(HttpClient client, string url, string fileName, IProgress? progress, CancellationToken token = default) - { - ArgumentNullException.ThrowIfNull(url); - ArgumentNullException.ThrowIfNull(fileName); - if (File.Exists(fileName)) - { - File.Delete(fileName); - } - - using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); - - if (!response.IsSuccessStatusCode) - { - throw new Exception(response.StatusCode.ToString()); - } - - var total = response.Content.Headers.ContentLength ?? -1L; - var canReportProgress = total != -1 && progress != null; - - await using var stream = await response.Content.ReadAsStreamAsync(token); - await using var file = File.Create(fileName); - var totalRead = 0L; - var buffer = new byte[1024 * 1024]; - var progressPercentage = 0; - - while (true) - { - token.ThrowIfCancellationRequested(); - - var read = await stream.ReadAsync(buffer, token); - totalRead += read; - - if (read == 0) - { - break; - } - await file.WriteAsync(buffer.AsMemory(0, read), token); - - if (canReportProgress) - { - var percent = (int)(100.0 * totalRead / total); - //if (progressPercentage != percent && percent % 10 == 0) - { - progressPercentage = percent; - progress?.Report(percent); - } - } - } - if (canReportProgress) - { - progress?.Report(101); - } - } - - public async Task DownloadDataAsync4Speed(HttpClient client, string url, IProgress progress, CancellationToken token = default) - { - if (url.IsNullOrEmpty()) - { - throw new ArgumentNullException(nameof(url)); - } - - var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); - - if (!response.IsSuccessStatusCode) - { - throw new Exception(response.StatusCode.ToString()); - } - - //var total = response.Content.Headers.ContentLength.HasValue ? response.Content.Headers.ContentLength.Value : -1L; - //var canReportProgress = total != -1 && progress != null; - - await using var stream = await response.Content.ReadAsStreamAsync(token); - var totalRead = 0L; - var buffer = new byte[1024 * 64]; - var isMoreToRead = true; - var progressSpeed = string.Empty; - var totalDatetime = DateTime.Now; - var totalSecond = 0; - - do - { - if (token.IsCancellationRequested) - { - if (totalRead > 0) - { - return; - } - else - { - token.ThrowIfCancellationRequested(); - } - } - - var read = await stream.ReadAsync(buffer, token); - - if (read == 0) - { - isMoreToRead = false; - } - else - { - var data = new byte[read]; - buffer.ToList().CopyTo(0, data, 0, read); - - totalRead += read; - - var ts = DateTime.Now - totalDatetime; - if (progress != null && ts.Seconds > totalSecond) - { - totalSecond = ts.Seconds; - var speed = (totalRead * 1d / ts.TotalMilliseconds / 1000).ToString("#0.0"); - if (progressSpeed != speed) - { - progressSpeed = speed; - progress.Report(speed); - } - } - } - } while (isMoreToRead); - } - - public async Task GetRealPingTime(string url, IWebProxy? webProxy, int downloadTimeout) - { - var responseTime = -1; - try - { - using var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(downloadTimeout)); - using var client = new HttpClient(new SocketsHttpHandler() - { - Proxy = webProxy, - UseProxy = webProxy != null - }); - - List oneTime = new(); - for (var i = 0; i < 2; i++) - { - var timer = Stopwatch.StartNew(); - await client.GetAsync(url, cts.Token).ConfigureAwait(false); - timer.Stop(); - oneTime.Add((int)timer.Elapsed.TotalMilliseconds); - await Task.Delay(100); - } - responseTime = oneTime.Where(x => x > 0).OrderBy(x => x).FirstOrDefault(); - } - catch //(Exception ex) - { - //Utile.SaveLog(ex.Message, ex); - } - return responseTime; - } } diff --git a/v2rayN/ServiceLib/Helper/SqliteHelper.cs b/v2rayN/ServiceLib/Helper/SqliteHelper.cs index 959d5ff6..83c9bf90 100644 --- a/v2rayN/ServiceLib/Helper/SqliteHelper.cs +++ b/v2rayN/ServiceLib/Helper/SqliteHelper.cs @@ -1,5 +1,4 @@ using System.Collections; -using SQLite; namespace ServiceLib.Helper; diff --git a/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs b/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs new file mode 100644 index 00000000..75e3e00e --- /dev/null +++ b/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs @@ -0,0 +1,301 @@ +namespace ServiceLib.Manager; + +/// +/// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.). +/// +public class ActionPrecheckManager(Config config) +{ + private static readonly Lazy _instance = new(() => new ActionPrecheckManager(AppManager.Instance.Config)); + public static ActionPrecheckManager Instance => _instance.Value; + + private readonly Config _config = config; + + public async Task> Check(string? indexId) + { + if (indexId.IsNullOrEmpty()) + { + return [ResUI.PleaseSelectServer]; + } + + var item = await AppManager.Instance.GetProfileItem(indexId); + if (item is null) + { + return [ResUI.PleaseSelectServer]; + } + + return await Check(item); + } + + public async Task> Check(ProfileItem? item) + { + if (item is null) + { + return [ResUI.PleaseSelectServer]; + } + + var errors = new List(); + + errors.AddRange(await ValidateCurrentNodeAndCoreSupport(item)); + errors.AddRange(await ValidateRelatedNodesExistAndValid(item)); + + return errors; + } + + private async Task> ValidateCurrentNodeAndCoreSupport(ProfileItem item) + { + if (item.ConfigType == EConfigType.Custom) + { + return []; + } + var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); + return await ValidateNodeAndCoreSupport(item, coreType); + } + + private async Task> ValidateNodeAndCoreSupport(ProfileItem item, ECoreType? coreType = null) + { + var errors = new List(); + + coreType ??= AppManager.Instance.GetCoreType(item, item.ConfigType); + + if (item.ConfigType is EConfigType.Custom) + { + errors.Add(string.Format(ResUI.NotSupportProtocol, item.ConfigType.ToString())); + return errors; + } + + if (!item.IsComplex()) + { + if (item.Address.IsNullOrEmpty()) + { + errors.Add(string.Format(ResUI.InvalidProperty, "Address")); + return errors; + } + + if (item.Port is <= 0 or >= 65536) + { + errors.Add(string.Format(ResUI.InvalidProperty, "Port")); + return errors; + } + + switch (item.ConfigType) + { + case EConfigType.VMess: + if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) + { + errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + } + + break; + + case EConfigType.VLESS: + if (item.Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(item.Id) && item.Id.Length > 30)) + { + errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + } + + if (!Global.Flows.Contains(item.Flow)) + { + errors.Add(string.Format(ResUI.InvalidProperty, "Flow")); + } + + break; + + case EConfigType.Shadowsocks: + if (item.Id.IsNullOrEmpty()) + { + errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + } + + if (string.IsNullOrEmpty(item.Security) || !Global.SsSecuritiesInSingbox.Contains(item.Security)) + { + errors.Add(string.Format(ResUI.InvalidProperty, "Security")); + } + + break; + } + + if (item.ConfigType is EConfigType.VLESS or EConfigType.Trojan + && item.StreamSecurity == Global.StreamSecurityReality + && item.PublicKey.IsNullOrEmpty()) + { + errors.Add(string.Format(ResUI.InvalidProperty, "PublicKey")); + } + + if (errors.Count > 0) + { + return errors; + } + } + + if (item.ConfigType.IsGroupType()) + { + ProfileGroupItemManager.Instance.TryGet(item.IndexId, out var group); + if (group is null || group.NotHasChild()) + { + errors.Add(string.Format(ResUI.GroupEmpty, item.Remarks)); + return errors; + } + + var hasCycle = ProfileGroupItemManager.HasCycle(item.IndexId); + if (hasCycle) + { + errors.Add(string.Format(ResUI.GroupSelfReference, item.Remarks)); + return errors; + } + + var childIds = Utils.String2List(group.ChildItems) ?? []; + var subItems = await ProfileGroupItemManager.GetSubChildProfileItems(group); + childIds.AddRange(subItems.Select(p => p.IndexId)); + + foreach (var child in childIds) + { + var childErrors = new List(); + if (child.IsNullOrEmpty()) + { + continue; + } + + var childItem = await AppManager.Instance.GetProfileItem(child); + if (childItem is null) + { + childErrors.Add(string.Format(ResUI.NodeTagNotExist, child)); + continue; + } + + if (childItem.ConfigType is EConfigType.Custom or EConfigType.ProxyChain) + { + childErrors.Add(string.Format(ResUI.InvalidProperty, childItem.Remarks)); + continue; + } + + childErrors.AddRange(await ValidateNodeAndCoreSupport(childItem, coreType)); + errors.AddRange(childErrors); + } + return errors; + } + + var net = item.GetNetwork() ?? item.Network; + + if (coreType == ECoreType.sing_box) + { + // sing-box does not support xhttp / kcp + // sing-box does not support transports like ws/http/httpupgrade/etc. when the node is not vmess/trojan/vless + if (net is nameof(ETransport.kcp) or nameof(ETransport.xhttp)) + { + errors.Add(string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net)); + return errors; + } + + if (item.ConfigType is not (EConfigType.VMess or EConfigType.VLESS or EConfigType.Trojan)) + { + if (net is nameof(ETransport.ws) or nameof(ETransport.http) or nameof(ETransport.h2) or nameof(ETransport.quic) or nameof(ETransport.httpupgrade)) + { + errors.Add(string.Format(ResUI.CoreNotSupportProtocolTransport, nameof(ECoreType.sing_box), item.ConfigType.ToString(), net)); + return errors; + } + } + } + else if (coreType is ECoreType.Xray) + { + // Xray core does not support these protocols + if (!Global.XraySupportConfigType.Contains(item.ConfigType) + && !item.IsComplex()) + { + errors.Add(string.Format(ResUI.CoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType.ToString())); + return errors; + } + } + + return errors; + } + + private async Task> ValidateRelatedNodesExistAndValid(ProfileItem? item) + { + var errors = new List(); + errors.AddRange(await ValidateProxyChainedNodeExistAndValid(item)); + errors.AddRange(await ValidateRoutingNodeExistAndValid(item)); + return errors; + } + + private async Task> ValidateProxyChainedNodeExistAndValid(ProfileItem? item) + { + var errors = new List(); + if (item is null) + { + return errors; + } + + // prev node and next node + var subItem = await AppManager.Instance.GetSubItem(item.Subid); + if (subItem is null) + { + return errors; + } + + var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); + var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); + + await CollectProxyChainedNodeValidation(prevNode, subItem.PrevProfile, coreType, errors); + await CollectProxyChainedNodeValidation(nextNode, subItem.NextProfile, coreType, errors); + + return errors; + } + + private async Task CollectProxyChainedNodeValidation(ProfileItem? node, string tag, ECoreType coreType, List errors) + { + if (node is not null) + { + var nodeErrors = await ValidateNodeAndCoreSupport(node, coreType); + errors.AddRange(nodeErrors.Select(s => ResUI.ProxyChainedPrefix + s)); + } + else if (tag.IsNotEmpty()) + { + errors.Add(ResUI.ProxyChainedPrefix + string.Format(ResUI.NodeTagNotExist, tag)); + } + } + + private async Task> ValidateRoutingNodeExistAndValid(ProfileItem? item) + { + var errors = new List(); + + if (item is null) + { + return errors; + } + + var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); + var routing = await ConfigHandler.GetDefaultRouting(_config); + if (routing == null) + { + return errors; + } + + var rules = JsonUtils.Deserialize>(routing.RuleSet); + foreach (var ruleItem in rules ?? []) + { + if (!ruleItem.Enabled) + { + continue; + } + + var outboundTag = ruleItem.OutboundTag; + if (outboundTag.IsNullOrEmpty() || Global.OutboundTags.Contains(outboundTag)) + { + continue; + } + + var tagItem = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); + if (tagItem is null) + { + errors.Add(ResUI.RoutingRuleOutboundPrefix + string.Format(ResUI.NodeTagNotExist, outboundTag)); + continue; + } + + var tagErrors = await ValidateNodeAndCoreSupport(tagItem, coreType); + errors.AddRange(tagErrors.Select(s => ResUI.RoutingRuleOutboundPrefix + s)); + } + + return errors; + } +} diff --git a/v2rayN/ServiceLib/Manager/AppManager.cs b/v2rayN/ServiceLib/Manager/AppManager.cs index 33125289..c66f76df 100644 --- a/v2rayN/ServiceLib/Manager/AppManager.cs +++ b/v2rayN/ServiceLib/Manager/AppManager.cs @@ -1,5 +1,3 @@ -using System.Reactive; - namespace ServiceLib.Manager; public sealed class AppManager @@ -10,7 +8,6 @@ public sealed class AppManager private Config _config; private int? _statePort; private int? _statePort2; - private Job? _processJob; public static AppManager Instance => _instance.Value; public Config Config => _config; @@ -67,6 +64,7 @@ public sealed class AppManager SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); + SQLiteHelper.Instance.CreateTable(); return true; } @@ -96,7 +94,7 @@ public sealed class AppManager Logging.SaveLog("AppExitAsync Begin"); await SysProxyHandler.UpdateSysProxy(_config, true); - AppEvents.AppExitRequested.OnNext(Unit.Default); + AppEvents.AppExitRequested.Publish(); await Task.Delay(50); //Wait for AppExitRequested to be processed await ConfigHandler.SaveConfig(_config); @@ -119,7 +117,13 @@ public sealed class AppManager public void Shutdown(bool byUser) { - AppEvents.ShutdownRequested.OnNext(byUser); + AppEvents.ShutdownRequested.Publish(byUser); + } + + public async Task RebootAsAdmin() + { + ProcUtils.RebootAsAdmin(); + await AppManager.Instance.AppExitAsync(true); } #endregion App @@ -132,21 +136,6 @@ public sealed class AppManager return localPort + (int)protocol; } - public void AddProcess(nint processHandle) - { - if (Utils.IsWindows()) - { - _processJob ??= new(); - try - { - _processJob?.AddProcess(processHandle); - } - catch - { - } - } - } - #endregion Config #region SqliteHelper @@ -219,6 +208,15 @@ public sealed class AppManager return await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(it => it.Remarks == remarks); } + public async Task GetProfileGroupItem(string indexId) + { + if (indexId.IsNullOrEmpty()) + { + return null; + } + return await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(it => it.IndexId == indexId); + } + public async Task?> RoutingItems() { return await SQLiteHelper.Instance.TableAsync().OrderBy(t => t.Sort).ToListAsync(); diff --git a/v2rayN/ServiceLib/Manager/CertPemManager.cs b/v2rayN/ServiceLib/Manager/CertPemManager.cs new file mode 100644 index 00000000..9421aabf --- /dev/null +++ b/v2rayN/ServiceLib/Manager/CertPemManager.cs @@ -0,0 +1,415 @@ +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace ServiceLib.Manager; + +/// +/// Manager for certificate operations with CA pinning to prevent MITM attacks +/// +public class CertPemManager +{ + private static readonly string _tag = "CertPemManager"; + private static readonly Lazy _instance = new(() => new()); + public static CertPemManager Instance => _instance.Value; + + /// + /// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks + /// + private static readonly HashSet TrustedCaThumbprints = new(StringComparer.OrdinalIgnoreCase) + { + "EBD41040E4BB3EC742C9E381D31EF2A41A48B6685C96E7CEF3C1DF6CD4331C99", // GlobalSign Root CA + "6DC47172E01CBCB0BF62580D895FE2B8AC9AD4F873801E0C10B9C837D21EB177", // Entrust.net Premium 2048 Secure Server CA + "73C176434F1BC6D5ADF45B0E76E727287C8DE57616C1E6E6141A2B2CBC7D8E4C", // Entrust Root Certification Authority + "D8E0FEBC1DB2E38D00940F37D27D41344D993E734B99D5656D9778D4D8143624", // Certum Root CA + "D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root + "85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2 + "18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3 + "CECDDC905099D8DADFC5B1D209B737CBE2C18CFB2C10C0FF0BCF0D3286FC1AA2", // XRamp Global CA Root + "C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA + "1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA + "3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA + "4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA + "7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA + "62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2 + "F1C1B50AE5A20DD8030EC9F6BC24823DD367B5255759B4E71B61FCE9F7375D73", // SecureTrust CA + "4200F5043AC8590EBB527D209ED1503029FBCBD41CA1B506EC27F15ADE7DAC69", // Secure Global CA + "0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority + "1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority + "41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA + "E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna + "C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority + "EAA962C4FA4A6BAFEBE415196D351CCD888D4F53F3FA8AE6D7C466A94E6042BB", // certSIGN ROOT CA + "6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány + "3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009 + "CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3 + "2530CC8E98321502BAD96F9B1FBA1B099E2D299E0F4548BB914F363BC0D4531F", // Izenpe.com + "45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2 + "2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2 + "568D6905A2C88708A4B3025190EDCFEDB1974A606A13C6E5290FCB2AE63EDAB5", // Starfield Services Root Certificate Authority - G2 + "0376AB1D54C5F9803CE4B2E201A0EE7EEF7B57B636E8A93C9B8D4860C96F5FA7", // AffirmTrust Commercial + "0A81EC5A929777F145904AF38D5D509F66B5E2C58FCDB531058B0E17F3F0B41B", // AffirmTrust Networking + "70A73F7F376B60074248904534B11482D5BF0E698ECC498DF52577EBF2E93B9A", // AffirmTrust Premium + "BD71FDF6DA97E4CF62D1647ADD2581B07D79ADF8397EB4ECBA9C5E8488821423", // AffirmTrust Premium ECC + "5C58468D55F58E497E743982D2B50010B6D165374ACF83A7D4A32DB768C4408E", // Certum Trusted Network CA + "BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority + "513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2 + "55926084EC963A64B96E2ABE01CE0BA86A64FBFEBCC7AAB5AFC155B37FD76066", // Actalis Authentication Root CA + "9A114025197C5BB95D94E63D55CD43790847B646B23CDF11ADA4A00EFF15FB48", // Buypass Class 2 Root CA + "EDF7EBBCA27A2A384D387B7D4010C666E2EDB4843E4C29B4AE1D5B9332E6B24D", // Buypass Class 3 Root CA + "FD73DAD31C644FF1B43BEF0CCDDA96710B9CD9875ECA7E31707AF3E96D522BBD", // T-TeleSec GlobalRoot Class 3 + "49E7A442ACF0EA6287050054B52564B650E4F49E42E348D6AA38E039E957B1C1", // D-TRUST Root Class 3 CA 2 2009 + "EEC5496B988CE98625B934092EEC2908BED0B0F316C2D4730C84EAF1F3D34881", // D-TRUST Root Class 3 CA 2 EV 2009 + "E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2 + "9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1 + "59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA + "DD6936FE21F8F077C123A1A521C12224F72255B73E03A7260693E8A24B0FA389", // TeliaSonera Root CA v1 + "91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2 + "F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011 + "8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3 + "8FE4FB0AF93A4D0D67DB0BEBB23E37C71BF325DCBCDD240EA04DAF58B47E1840", // QuoVadis Root CA 2 G3 + "88EF81DE202EB018452E43F864725CEA5FBD1FC2D9D205730709C5D8B8690F46", // QuoVadis Root CA 3 G3 + "7D05EBB682339F8C9451EE094EEBFEFA7953A114EDB2F44949452FAB7D2FC185", // DigiCert Assured ID Root G2 + "7E37CB8B4C47090CAB36551BA6F45DB840680FBA166A952DB100717F43053FC2", // DigiCert Assured ID Root G3 + "CB3CCBB76031E5E0138F8DD39A23F9DE47FFC35E43C1144CEA27D46A5AB1CB5F", // DigiCert Global Root G2 + "31AD6648F8104138C738F39EA4320133393E3A18CC02296EF97C2AC9EF6731D0", // DigiCert Global Root G3 + "552F7BDCF1A7AF9E6CE672017F4F12ABF77240C78E761AC203D1D9D20AC89988", // DigiCert Trusted Root G4 + "52F0E1C4E58EC629291B60317F074671B85D7EA80D5B07273463534B32B40234", // COMODO RSA Certification Authority + "E793C9B02FD8AA13E21C31228ACCB08119643B749C898964B1746D46C3D4CBD2", // USERTrust RSA Certification Authority + "4FF460D54B9C86DABFBCFC5712E0400D2BED3FBC4D4FBDAA86E06ADCD2A9AD7A", // USERTrust ECC Certification Authority + "179FBC148A3DD00FD24EA13458CC43BFA7F59C8182D783A513F6EBEC100C8924", // GlobalSign ECC Root CA - R5 + "3C4FB0B95AB8B30032F432B86F535FE172C185D0FD39865837CF36187FA6F428", // Staat der Nederlanden Root CA - G3 + "5D56499BE4D2E08BCFCAD08A3E38723D50503BDE706948E42F55603019E528AE", // IdenTrust Commercial Root CA 1 + "30D0895A9A448A262091635522D1F52010B5867ACAE12C78EF958FD4F4389F2F", // IdenTrust Public Sector Root CA 1 + "43DF5774B03E7FEF5FE40D931A7BEDF1BB2E6B42738C4E6D3841103D3AA7F339", // Entrust Root Certification Authority - G2 + "02ED0EB28C14DA45165C566791700D6451D7FB56F0B2AB1D3B8EB070E56EDFF5", // Entrust Root Certification Authority - EC1 + "5CC3D78E4E1D5E45547A04E6873E64F90CF9536D1CCC2EF800F355C4C5FD70FD", // CFCA EV ROOT + "6B9C08E86EB0F767CFAD65CD98B62149E5494A67F5845E7BD1ED019F27B86BD6", // OISTE WISeKey Global Root GB CA + "A1339D33281A0B56E557D3D32B1CE7F9367EB094BD5FA72A7E5004C8DED7CAFE", // SZAFIR ROOT CA2 + "B676F2EDDAE8775CD36CB0F63CD1D4603961F49E6265BA013A2F0307B6D0B804", // Certum Trusted Network CA 2 + "A040929A02CE53B4ACF4F2FFC6981CE4496F755E6D45FE0B2A692BCD52523F36", // Hellenic Academic and Research Institutions RootCA 2015 + "44B545AA8A25E65A73CA15DC27FC36D24C1CB9953A066539B11582DC487B4833", // Hellenic Academic and Research Institutions ECC RootCA 2015 + "96BCEC06264976F37460779ACF28C5A7CFE8A3C0AAE11A8FFCEE05C0BDDF08C6", // ISRG Root X1 + "EBC5570C29018C4D67B1AA127BAF12F703B4611EBC17B7DAB5573894179B93FA", // AC RAIZ FNMT-RCM + "8ECDE6884F3D87B1125BA31AC3FCB13D7016DE7F57CC904FE1CB97C6AE98196E", // Amazon Root CA 1 + "1BA5B2AA8C65401A82960118F80BEC4F62304D83CEC4713A19C39C011EA46DB4", // Amazon Root CA 2 + "18CE6CFE7BF14E60B2E347B8DFE868CB31D02EBB3ADA271569F50343B46DB3A4", // Amazon Root CA 3 + "E35D28419ED02025CFA69038CD623962458DA5C695FBDEA3C22B0BFB25897092", // Amazon Root CA 4 + "A1A86D04121EB87F027C66F53303C28E5739F943FC84B38AD6AF009035DD9457", // D-TRUST Root CA 3 2013 + "46EDC3689046D53A453FB3104AB80DCAEC658B2660EA1629DD7E867990648716", // TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 + "BFFF8FD04433487D6A8AA60C1A29767A9FC2BBB05E420F713A13B992891D3893", // GDCA TrustAUTH R5 ROOT + "85666A562EE0BE5CE925C1D8890A6F76A87EC16D4D7D5F29EA7419CF20123B69", // SSL.com Root Certification Authority RSA + "3417BB06CC6007DA1B961C920B8AB4CE3FAD820E4AA30B9ACBC4A74EBDCEBC65", // SSL.com Root Certification Authority ECC + "2E7BF16CC22485A7BBE2AA8696750761B0AE39BE3B2FE9D0CC6D4EF73491425C", // SSL.com EV Root Certification Authority RSA R2 + "22A2C1F7BDED704CC1E701B5F408C310880FE956B5DE2A4A44F99C873A25A7C8", // SSL.com EV Root Certification Authority ECC + "2CABEAFE37D06CA22ABA7391C0033D25982952C453647349763A3AB5AD6CCF69", // GlobalSign Root CA - R6 + "8560F91C3624DABA9570B5FEA0DBE36FF11A8323BE9486854FB3F34A5571198D", // OISTE WISeKey Global Root GC CA + "9BEA11C976FE014764C1BE56A6F914B5A560317ABD9988393382E5161AA0493C", // UCA Global G2 Root + "D43AF9B35473755C9684FC06D7D8CB70EE5C28E773FB294EB41EE71722924D24", // UCA Extended Validation Root + "D48D3D23EEDB50A459E55197601C27774B9D7B18C94D5A059511A10250B93168", // Certigna Root CA + "40F6AF0346A99AA1CD1D555A4E9CCE62C7F9634603EE406615833DC8C8D00367", // emSign Root CA - G1 + "86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B", // emSign ECC Root CA - G3 + "125609AA301DA0A249B97A8239CB6A34216F44DCAC9F3954B14292F2E8C8608F", // emSign Root CA - C1 + "BC4D809B15189D78DB3E1D8CF4F9726A795DA1643CA5F1358E1DDB0EDC0D7EB3", // emSign ECC Root CA - C3 + "5A2FC03F0C83B090BBFA40604B0988446C7636183DF9846E17101A447FB8EFD6", // Hongkong Post Root CA 3 + "DB3517D1F6732A2D5AB97C533EC70779EE3270A62FB4AC4238372460E6F01E88", // Entrust Root Certification Authority - G4 + "358DF39D764AF9E1B766E9C972DF352EE15CFAC227AF6AD1D70E8E4A6EDCBA02", // Microsoft ECC Root Certificate Authority 2017 + "C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017 + "BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017 + "657CFE2FA73FAA38462571F332A2363A46FCE7020951710702CDFBB6EEDA3305", // certSIGN Root CA G2 + "97552015F5DDFC3C8788C006944555408894450084F100867086BC1A2BB58DC8", // Trustwave Global Certification Authority + "945BBC825EA554F489D1FD51A73DDF2EA624AC7019A05205225C22A78CCFA8B4", // Trustwave Global ECC P256 Certification Authority + "55903859C8C0C3EBB8759ECE4E2557225FF5758BBD38EBD48276601E1BD58097", // Trustwave Global ECC P384 Certification Authority + "88F438DCF8FFD1FA8F429115FFE5F82AE1E06E0C70C375FAAD717B34A49E7265", // NAVER Global Root Certification Authority + "554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS + "319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45 + "5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45 + "4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46 + "CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46 + "9A296A5182D1D451A2E37F439B74DAAFA267523329F90F9A0D2007C334E23C9A", // GLOBALTRUST 2020 + "FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA + "6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA + "FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA + "2E44102AB58CB85419451C8E19D9ACF3662CAFBC614B6A53960A30F7D0E2EB41", // TunTrust Root CA + "D95D0E8EDA79525BF9BEB11B14D2100D3294985F0C62D9FABD9CD999ECCB7B1D", // HARICA TLS RSA Root CA 2021 + "3F99CC474ACFCE4DFED58794665E478D1547739F2E780F1BB4CA9B133097D401", // HARICA TLS ECC Root CA 2021 + "1BE7ABE30686B16348AFD1C61B6866A0EA7F4821E67D5E8AF937CF8011BC750D", // HARICA Client RSA Root CA 2021 + "8DD4B5373CB0DE36769C12339280D82746B3AA6CD426E797A31BABE4279CF00B", // HARICA Client ECC Root CA 2021 + "57DE0583EFD2B26E0361DA99DA9DF4648DEF7EE8441C3B728AFA9BCDE0F9B26A", // Autoridad de Certificacion Firmaprofesional CIF A62634068 + "30FBBA2C32238E2A98547AF97931E550428B9B3F1C8EEB6633DCFA86C5B27DD3", // vTrus ECC Root CA + "8A71DE6559336F426C26E53880D00D88A18DA4C6A91F0DCB6194E206C5C96387", // vTrus Root CA + "69729B8E15A86EFC177A57AFB7171DFC64ADD28C2FCA8CF1507E34453CCB1470", // ISRG Root X2 + "F015CE3CC239BFEF064BE9F1D2C417E1A0264A0A94BE1F0C8D121864EB6949CC", // HiPKI Root CA - G1 + "B085D70B964F191A73E4AF0D54AE7A0E07AAFDAF9B71DD0862138AB7325A24A2", // GlobalSign ECC Root CA - R4 + "D947432ABDE7B7FA90FC2E6B59101B1280E0E1C7E4E40FA3C6887FFF57A7F4CF", // GTS Root R1 + "8D25CD97229DBF70356BDA4EB3CC734031E24CF00FAFCFD32DC76EB5841C7EA8", // GTS Root R2 + "34D8A73EE208D9BCDB0D956520934B4E40E69482596E8B6F73C8426B010A6F48", // GTS Root R3 + "349DFA4058C5E263123B398AE795573C4E1313C83FE68F93556CD5E8031B3C7D", // GTS Root R4 + "242B69742FCB1E5B2ABF98898B94572187544E5B4D9911786573621F6A74B82C", // Telia Root CA v2 + "E59AAA816009C22BFF5B25BAD37DF306F049797C1F81D85AB089E657BD8F0044", // D-TRUST BR Root CA 1 2020 + "08170D1AA36453901A2F959245E347DB0C8D37ABAABC56B81AA100DC958970DB", // D-TRUST EV Root CA 1 2020 + "018E13F0772532CF809BD1B17281867283FC48C6E13BE9C69812854A490C1B05", // DigiCert TLS ECC P384 Root G5 + "371A00DC0533B3721A7EEB40E8419E70799D2B0A0F2C1D80693165F7CEC4AD75", // DigiCert TLS RSA4096 Root G5 + "E8E8176536A60CC2C4E10187C3BEFCA20EF263497018F566D5BEA0F94D0C111B", // DigiCert SMIME ECC P384 Root G5 + "90370D3EFA88BF58C30105BA25104A358460A7FA52DFC2011DF233A0F417912A", // DigiCert SMIME RSA4096 Root G5 + "77B82CD8644C4305F7ACC5CB156B45675004033D51C60C6202A8E0C33467D3A0", // Certainly Root R1 + "B4585F22E4AC756A4E8612A1361C5D9D031A93FD84FEBB778FA3068B0FC42DC2", // Certainly Root E1 + "82BD5D851ACF7F6E1BA7BFCBC53030D0E7BC3C21DF772D858CAB41D199BDF595", // DIGITALSIGN GLOBAL ROOT RSA CA + "261D7114AE5F8FF2D8C7209A9DE4289E6AFC9D717023D85450909199F1857CFE", // DIGITALSIGN GLOBAL ROOT ECDSA CA + "E74FBDA55BD564C473A36B441AA799C8A68E077440E8288B9FA1E50E4BBACA11", // Security Communication ECC RootCA1 + "F3896F88FE7C0A882766A7FA6AD2749FB57A7F3E98FB769C1FA7B09C2C44D5AE", // BJCA Global Root CA1 + "574DF6931E278039667B720AFDC1600FC27EB66DD3092979FB73856487212882", // BJCA Global Root CA2 + "48E1CF9E43B688A51044160F46D773B8277FE45BEAAD0E4DF90D1974382FEA99", // LAWtrust Root CA2 (4096) + "22D9599234D60F1D4BC7C7E96F43FA555B07301FD475175089DAFB8C25E477B3", // Sectigo Public Email Protection Root E46 + "D5917A7791EB7CF20A2E57EB98284A67B28A57E89182DA53D546678C9FDE2B4F", // Sectigo Public Email Protection Root R46 + "C90F26F0FB1B4018B22227519B5CA2B53E2CA5B3BE5CF18EFE1BEF47380C5383", // Sectigo Public Server Authentication Root E46 + "7BB647A62AEEAC88BF257AA522D01FFEA395E0AB45C73F93F65654EC38F25A06", // Sectigo Public Server Authentication Root R46 + "8FAF7D2E2CB4709BB8E0B33666BF75A5DD45B5DE480F8EA8D4BFE6BEBC17F2ED", // SSL.com TLS RSA Root CA 2022 + "C32FFD9F46F936D16C3673990959434B9AD60AAFBB9E7CF33654F144CC1BA143", // SSL.com TLS ECC Root CA 2022 + "AD7DD58D03AEDB22A30B5084394920CE12230C2D8017AD9B81AB04079BDD026B", // SSL.com Client ECC Root CA 2022 + "1D4CA4A2AB21D0093659804FC0EB2175A617279B56A2475245C9517AFEB59153", // SSL.com Client RSA Root CA 2022 + "E38655F4B0190C84D3B3893D840A687E190A256D98052F159E6D4A39F589A6EB", // Atos TrustedRoot Root CA ECC G2 2020 + "78833A783BB2986C254B9370D3C20E5EBA8FA7840CBF63FE17297A0B0119685E", // Atos TrustedRoot Root CA RSA G2 2020 + "B2FAE53E14CCD7AB9212064701AE279C1D8988FACB775FA8A008914E663988A8", // Atos TrustedRoot Root CA ECC TLS 2021 + "81A9088EA59FB364C548A6F85559099B6F0405EFBF18E5324EC9F457BA00112F", // Atos TrustedRoot Root CA RSA TLS 2021 + "E0D3226AEB1163C2E48FF9BE3B50B4C6431BE7BB1EACC5C36B5D5EC509039A08", // TrustAsia Global Root CA G3 + "BE4B56CB5056C0136A526DF444508DAA36A0B54F42E4AC38F72AF470E479654C", // TrustAsia Global Root CA G4 + "D92C171F5CF890BA428019292927FE22F3207FD2B54449CB6F675AF4922146E2", // D-Trust SBR Root CA 1 2022 + "DBA84DD7EF622D485463A90137EA4D574DF8550928F6AFA03B4D8B1141E636CC", // D-Trust SBR Root CA 2 2022 + "3AE6DF7E0D637A65A8C81612EC6F9A142F85A16834C10280D88E707028518755", // Telekom Security SMIME ECC Root 2021 + "578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020 + "78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023 + "EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023 + "BEF256DAF26E9C69BDEC1602359798F3CAF71821A03E018257C53C65617F3D4A", // FIRMAPROFESIONAL CA ROOT-A WEB + "3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA + "3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2 + "3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12 + "4B009C1034494F9AB56BBA3BA1D62731FC4D20D8955ADCEC10A925607261E338", // SecureSign Root CA14 + "E778F0F095FE843729CD1A0082179E5314A9C291442805E1FB1D8FB6B8886C3A", // SecureSign Root CA15 + "0552E6F83FDF65E8FA9670E666DF28A4E21340B510CBE52566F97C4FB94B2BD1", // D-TRUST BR Root CA 2 2023 + "436472C1009A325C54F1A5BBB5468A7BAEECCBE05DE5F099CB70D3FE41E13C16", // TrustAsia SMIME ECC Root CA + "C7796BEB62C101BB143D262A7C96A0C6168183223EF50D699632D86E03B8CC9B", // TrustAsia SMIME RSA Root CA + "C0076B9EF0531FB1A656D67C4EBE97CD5DBAA41EF44598ACC2489878C92D8711", // TrustAsia TLS ECC Root CA + "06C08D7DAFD876971EB1124FE67F847EC0C7A158D3EA53CBE940E2EA9791F4C3", // TrustAsia TLS RSA Root CA + "8E8221B2E7D4007836A1672F0DCC299C33BC07D316F132FA1A206D587150F1CE", // D-TRUST EV Root CA 2 2023 + "9A12C392BFE57891A0C545309D4D9FD567E480CB613D6342278B195C79A7931F", // SwissSign RSA SMIME Root CA 2022 - 1 + "193144F431E0FDDB740717D4DE926A571133884B4360D30E272913CBE660CE41", // SwissSign RSA TLS Root CA 2022 - 1 + "D9A32485A8CCA85539CEF12FFFFF711378A17851D73DA2732AB4302D763BD62B", // OISTE Client Root ECC G1 + "D02A0F994A868C66395F2E7A880DF509BD0C29C96DE16015A0FD501EDA4F96A9", // OISTE Client Root RSA G1 + "EEC997C0C30F216F7E3B8B307D2BAE42412D753FC8219DAFD1520B2572850F49", // OISTE Server Root ECC G1 + "9AE36232A5189FFDDB353DFD26520C015395D22777DAC59DB57B98C089A651E6", // OISTE Server Root RSA G1 + }; + + /// + /// Get certificate in PEM format from a server with CA pinning validation + /// + public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4) + { + try + { + var (domain, _, port, _) = Utils.ParseUrl(target); + + using var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(timeout)); + + using var client = new TcpClient(); + await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); + + using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate); + + var sslOptions = new SslClientAuthenticationOptions + { + TargetHost = serverName, + RemoteCertificateValidationCallback = ValidateServerCertificate + }; + + await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token); + + var remote = ssl.RemoteCertificate; + if (remote == null) + { + return (null, null); + } + + var leaf = new X509Certificate2(remote); + return (ExportCertToPem(leaf), null); + } + catch (OperationCanceledException) + { + Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds")); + return (null, $"Connection timeout after {timeout} seconds"); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + return (null, ex.Message); + } + } + + /// + /// Get certificate chain in PEM format from a server with CA pinning validation + /// + public async Task<(List, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4) + { + var pemList = new List(); + try + { + var (domain, _, port, _) = Utils.ParseUrl(target); + + using var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(timeout)); + + using var client = new TcpClient(); + await client.ConnectAsync(domain, port > 0 ? port : 443, cts.Token); + + using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate); + + var sslOptions = new SslClientAuthenticationOptions + { + TargetHost = serverName, + RemoteCertificateValidationCallback = ValidateServerCertificate + }; + + await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token); + + if (ssl.RemoteCertificate is not X509Certificate2 certChain) + { + return (pemList, null); + } + + var chain = new X509Chain(); + chain.Build(certChain); + + foreach (var element in chain.ChainElements) + { + var pem = ExportCertToPem(element.Certificate); + pemList.Add(pem); + } + + return (pemList, null); + } + catch (OperationCanceledException) + { + Logging.SaveLog(_tag, new TimeoutException($"Connection timeout after {timeout} seconds")); + return (pemList, $"Connection timeout after {timeout} seconds"); + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + return (pemList, ex.Message); + } + } + + /// + /// Validate server certificate with CA pinning + /// + private bool ValidateServerCertificate( + object sender, + X509Certificate? certificate, + X509Chain? chain, + SslPolicyErrors sslPolicyErrors) + { + if (certificate == null) + { + return false; + } + + // Check certificate name mismatch + if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch)) + { + return false; + } + + // Build certificate chain + var cert2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate); + var certChain = chain ?? new X509Chain(); + + certChain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + certChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; + certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; + certChain.ChainPolicy.VerificationTime = DateTime.Now; + + certChain.Build(cert2); + + // Find root CA + if (certChain.ChainElements.Count == 0) + { + return false; + } + + var rootCert = certChain.ChainElements[certChain.ChainElements.Count - 1].Certificate; + var rootThumbprint = rootCert.GetCertHashString(HashAlgorithmName.SHA256); + + return TrustedCaThumbprints.Contains(rootThumbprint); + } + + public static string ExportCertToPem(X509Certificate2 cert) + { + var der = cert.Export(X509ContentType.Cert); + var b64 = Convert.ToBase64String(der); + return $"-----BEGIN CERTIFICATE-----\n{b64}\n-----END CERTIFICATE-----\n"; + } + + /// + /// Parse concatenated PEM certificates string into a list of individual certificates + /// Normalizes format: removes line breaks from base64 content for better compatibility + /// + /// Concatenated PEM certificates string (supports both \r\n and \n line endings) + /// List of individual PEM certificate strings with normalized format + public static List ParsePemChain(string pemChain) + { + var certs = new List(); + if (string.IsNullOrWhiteSpace(pemChain)) + { + return certs; + } + + // Normalize line endings (CRLF -> LF) at the beginning + pemChain = pemChain.Replace("\r\n", "\n").Replace("\r", "\n"); + + const string beginMarker = "-----BEGIN CERTIFICATE-----"; + const string endMarker = "-----END CERTIFICATE-----"; + + var index = 0; + while (index < pemChain.Length) + { + var beginIndex = pemChain.IndexOf(beginMarker, index, StringComparison.Ordinal); + if (beginIndex == -1) + break; + + var endIndex = pemChain.IndexOf(endMarker, beginIndex, StringComparison.Ordinal); + if (endIndex == -1) + break; + + // Extract certificate content + var base64Start = beginIndex + beginMarker.Length; + var base64Content = pemChain.Substring(base64Start, endIndex - base64Start); + + // Remove all whitespace from base64 content + base64Content = new string(base64Content.Where(c => !char.IsWhiteSpace(c)).ToArray()); + + // Reconstruct with clean format: BEGIN marker + base64 (no line breaks) + END marker + var normalizedCert = $"{beginMarker}\n{base64Content}\n{endMarker}\n"; + certs.Add(normalizedCert); + + // Move to next certificate + index = endIndex + endMarker.Length; + } + + return certs; + } + + /// + /// Concatenate a list of PEM certificates into a single string + /// + /// List of individual PEM certificate strings + /// Concatenated PEM certificates string + public static string ConcatenatePemChain(IEnumerable pemList) + { + if (pemList == null) + { + return string.Empty; + } + + return string.Concat(pemList); + } +} diff --git a/v2rayN/ServiceLib/Manager/CoreAdminManager.cs b/v2rayN/ServiceLib/Manager/CoreAdminManager.cs index 90b47106..254688e4 100644 --- a/v2rayN/ServiceLib/Manager/CoreAdminManager.cs +++ b/v2rayN/ServiceLib/Manager/CoreAdminManager.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; -using System.Text; using CliWrap; using CliWrap.Buffered; @@ -31,58 +29,33 @@ public class CoreAdminManager await _updateFunc?.Invoke(notify, msg); } - public async Task RunProcessAsLinuxSudo(string fileName, CoreInfo coreInfo, string configPath) + public async Task RunProcessAsLinuxSudo(string fileName, CoreInfo coreInfo, string configPath) { StringBuilder sb = new(); sb.AppendLine("#!/bin/bash"); var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}"; - sb.AppendLine($"sudo -S {cmdLine}"); - var shFilePath = await FileManager.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true); + sb.AppendLine($"exec sudo -S -- {cmdLine}"); + var shFilePath = await FileUtils.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true); - Process proc = new() - { - StartInfo = new() - { - FileName = shFilePath, - Arguments = "", - WorkingDirectory = Utils.GetBinConfigPath(), - UseShellExecute = false, - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8, - } - }; + var procService = new ProcessService( + fileName: shFilePath, + arguments: "", + workingDirectory: Utils.GetBinConfigPath(), + displayLog: true, + redirectInput: true, + environmentVars: null, + updateFunc: _updateFunc + ); - void dataHandler(object sender, DataReceivedEventArgs e) - { - if (e.Data.IsNotEmpty()) - { - _ = UpdateFunc(false, e.Data + Environment.NewLine); - } - } + await procService.StartAsync(AppManager.Instance.LinuxSudoPwd); - proc.OutputDataReceived += dataHandler; - proc.ErrorDataReceived += dataHandler; - - proc.Start(); - proc.BeginOutputReadLine(); - proc.BeginErrorReadLine(); - - await Task.Delay(10); - await proc.StandardInput.WriteLineAsync(AppManager.Instance.LinuxSudoPwd); - - await Task.Delay(100); - if (proc is null or { HasExited: true }) + if (procService is null or { HasExited: true }) { throw new Exception(ResUI.FailedToRunCore); } + _linuxSudoPid = procService.Id; - _linuxSudoPid = proc.Id; - - return proc; + return procService; } public async Task KillProcessAsLinuxSudo() @@ -94,8 +67,8 @@ public class CoreAdminManager try { - var shellFileName = Utils.IsOSX() ? Global.KillAsSudoOSXShellFileName : Global.KillAsSudoLinuxShellFileName; - var shFilePath = await FileManager.CreateLinuxShellFile("kill_as_sudo.sh", EmbedUtils.GetEmbedText(shellFileName), true); + var shellFileName = Utils.IsMacOS() ? Global.KillAsSudoOSXShellFileName : Global.KillAsSudoLinuxShellFileName; + var shFilePath = await FileUtils.CreateLinuxShellFile("kill_as_sudo.sh", EmbedUtils.GetEmbedText(shellFileName), true); if (shFilePath.Contains(' ')) { shFilePath = shFilePath.AppendQuotes(); diff --git a/v2rayN/ServiceLib/Manager/CoreManager.cs b/v2rayN/ServiceLib/Manager/CoreManager.cs index 695508c2..8dbff9e2 100644 --- a/v2rayN/ServiceLib/Manager/CoreManager.cs +++ b/v2rayN/ServiceLib/Manager/CoreManager.cs @@ -1,6 +1,3 @@ -using System.Diagnostics; -using System.Text; - namespace ServiceLib.Manager; /// @@ -11,8 +8,9 @@ public class CoreManager private static readonly Lazy _instance = new(() => new()); public static CoreManager Instance => _instance.Value; private Config _config; - private Process? _process; - private Process? _processPre; + private WindowsJobService? _processJob; + private ProcessService? _processService; + private ProcessService? _processPreService; private bool _linuxSudo = false; private Func? _updateFunc; private const string _tag = "CoreHandler"; @@ -29,7 +27,7 @@ public class CoreManager var toPath = Utils.GetBinPath(""); if (fromPath != toPath) { - FileManager.CopyDirectory(fromPath, toPath, true, false); + FileUtils.CopyDirectory(fromPath, toPath, true, false); } } @@ -89,43 +87,37 @@ public class CoreManager await CoreStart(node); await CoreStartPreService(node); - if (_process != null) + if (_processService != null) { await UpdateFunc(true, $"{node.GetSummary()}"); } } - public async Task LoadCoreConfigSpeedtest(List selecteds) + public async Task LoadCoreConfigSpeedtest(List selecteds) { - var coreType = selecteds.Exists(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls) ? ECoreType.sing_box : ECoreType.Xray; + var coreType = selecteds.Any(t => Global.SingboxOnlyConfigType.Contains(t.ConfigType)) ? ECoreType.sing_box : ECoreType.Xray; var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false)); var configPath = Utils.GetBinConfigPath(fileName); var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, configPath, selecteds, coreType); await UpdateFunc(false, result.Msg); if (result.Success != true) { - return -1; + return null; } await UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))); await UpdateFunc(false, configPath); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); - var proc = await RunProcess(coreInfo, fileName, true, false); - if (proc is null) - { - return -1; - } - - return proc.Id; + return await RunProcess(coreInfo, fileName, true, false); } - public async Task LoadCoreConfigSpeedtest(ServerTestItem testItem) + public async Task LoadCoreConfigSpeedtest(ServerTestItem testItem) { var node = await AppManager.Instance.GetProfileItem(testItem.IndexId); if (node is null) { - return -1; + return null; } var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false)); @@ -133,18 +125,12 @@ public class CoreManager var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, node, testItem, configPath); if (result.Success != true) { - return -1; + return null; } var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); - var proc = await RunProcess(coreInfo, fileName, true, false); - if (proc is null) - { - return -1; - } - - return proc.Id; + return await RunProcess(coreInfo, fileName, true, false); } public async Task CoreStop() @@ -157,16 +143,18 @@ public class CoreManager _linuxSudo = false; } - if (_process != null) + if (_processService != null) { - await ProcUtils.ProcessKill(_process, Utils.IsWindows()); - _process = null; + await _processService.StopAsync(); + _processService.Dispose(); + _processService = null; } - if (_processPre != null) + if (_processPreService != null) { - await ProcUtils.ProcessKill(_processPre, Utils.IsWindows()); - _processPre = null; + await _processPreService.StopAsync(); + _processPreService.Dispose(); + _processPreService = null; } } catch (Exception ex) @@ -188,12 +176,12 @@ public class CoreManager { return; } - _process = proc; + _processService = proc; } private async Task CoreStartPreService(ProfileItem node) { - if (_process != null && !_process.HasExited) + if (_processService != null && !_processService.HasExited) { var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType); var itemSocks = await ConfigHandler.GetPreSocksItem(_config, node, coreType); @@ -210,7 +198,7 @@ public class CoreManager { return; } - _processPre = proc; + _processPreService = proc; } } } @@ -225,7 +213,7 @@ public class CoreManager #region Process - private async Task RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo) + private async Task RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo) { var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg); if (fileName.IsNullOrEmpty()) @@ -256,55 +244,48 @@ public class CoreManager } } - private async Task RunProcessNormal(string fileName, CoreInfo? coreInfo, string configPath, bool displayLog) + private async Task RunProcessNormal(string fileName, CoreInfo? coreInfo, string configPath, bool displayLog) { - Process proc = new() - { - StartInfo = new() - { - FileName = fileName, - Arguments = string.Format(coreInfo.Arguments, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath), - WorkingDirectory = Utils.GetBinConfigPath(), - UseShellExecute = false, - RedirectStandardOutput = displayLog, - RedirectStandardError = displayLog, - CreateNoWindow = true, - StandardOutputEncoding = displayLog ? Encoding.UTF8 : null, - StandardErrorEncoding = displayLog ? Encoding.UTF8 : null, - } - }; + var environmentVars = new Dictionary(); foreach (var kv in coreInfo.Environment) { - proc.StartInfo.Environment[kv.Key] = string.Format(kv.Value, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath); + environmentVars[kv.Key] = string.Format(kv.Value, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath); } - if (displayLog) - { - void dataHandler(object sender, DataReceivedEventArgs e) - { - if (e.Data.IsNotEmpty()) - { - _ = UpdateFunc(false, e.Data + Environment.NewLine); - } - } - proc.OutputDataReceived += dataHandler; - proc.ErrorDataReceived += dataHandler; - } - proc.Start(); + var procService = new ProcessService( + fileName: fileName, + arguments: string.Format(coreInfo.Arguments, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath), + workingDirectory: Utils.GetBinConfigPath(), + displayLog: displayLog, + redirectInput: false, + environmentVars: environmentVars, + updateFunc: _updateFunc + ); - if (displayLog) - { - proc.BeginOutputReadLine(); - proc.BeginErrorReadLine(); - } + await procService.StartAsync(); await Task.Delay(100); - AppManager.Instance.AddProcess(proc.Handle); - if (proc is null or { HasExited: true }) + + if (procService is null or { HasExited: true }) { throw new Exception(ResUI.FailedToRunCore); } - return proc; + AddProcessJob(procService.Handle); + + return procService; + } + + private void AddProcessJob(nint processHandle) + { + if (Utils.IsWindows()) + { + _processJob ??= new(); + try + { + _processJob?.AddProcess(processHandle); + } + catch { } + } } #endregion Process diff --git a/v2rayN/ServiceLib/Manager/NoticeManager.cs b/v2rayN/ServiceLib/Manager/NoticeManager.cs index f9e149ed..da034f02 100644 --- a/v2rayN/ServiceLib/Manager/NoticeManager.cs +++ b/v2rayN/ServiceLib/Manager/NoticeManager.cs @@ -11,7 +11,7 @@ public class NoticeManager { return; } - AppEvents.SendSnackMsgRequested.OnNext(content); + AppEvents.SendSnackMsgRequested.Publish(content); } public void SendMessage(string? content) @@ -20,7 +20,7 @@ public class NoticeManager { return; } - AppEvents.SendMsgViewRequested.OnNext(content); + AppEvents.SendMsgViewRequested.Publish(content); } public void SendMessageEx(string? content) diff --git a/v2rayN/ServiceLib/Manager/PacManager.cs b/v2rayN/ServiceLib/Manager/PacManager.cs index 10bedc29..92c0b5ba 100644 --- a/v2rayN/ServiceLib/Manager/PacManager.cs +++ b/v2rayN/ServiceLib/Manager/PacManager.cs @@ -1,6 +1,3 @@ -using System.Net.Sockets; -using System.Text; - namespace ServiceLib.Manager; public class PacManager @@ -8,7 +5,6 @@ public class PacManager private static readonly Lazy _instance = new(() => new PacManager()); public static PacManager Instance => _instance.Value; - private string _configPath; private int _httpPort; private int _pacPort; private TcpListener? _tcpListener; @@ -16,11 +12,10 @@ public class PacManager private bool _isRunning; private bool _needRestart = true; - public async Task StartAsync(string configPath, int httpPort, int pacPort) + public async Task StartAsync(int httpPort, int pacPort) { - _needRestart = configPath != _configPath || httpPort != _httpPort || pacPort != _pacPort || !_isRunning; + _needRestart = httpPort != _httpPort || pacPort != _pacPort || !_isRunning; - _configPath = configPath; _httpPort = httpPort; _pacPort = pacPort; @@ -35,22 +30,22 @@ public class PacManager private async Task InitText() { - var path = Path.Combine(_configPath, "pac.txt"); + var customSystemProxyPacPath = AppManager.Instance.Config.SystemProxyItem?.CustomSystemProxyPacPath; + var fileName = (customSystemProxyPacPath.IsNotEmpty() && File.Exists(customSystemProxyPacPath)) + ? customSystemProxyPacPath + : Path.Combine(Utils.GetConfigPath(), "pac.txt"); - // Delete the old pac file - if (File.Exists(path) && Utils.GetFileHash(path).Equals("b590c07280f058ef05d5394aa2f927fe")) - { - File.Delete(path); - } + // TODO: temporarily notify which script is being used + NoticeManager.Instance.SendMessage(fileName); - if (!File.Exists(path)) + if (!File.Exists(fileName)) { var pac = EmbedUtils.GetEmbedText(Global.PacFileName); - await File.AppendAllTextAsync(path, pac); + await File.AppendAllTextAsync(fileName, pac); } - var pacText = - (await File.ReadAllTextAsync(path)).Replace("__PROXY__", $"PROXY 127.0.0.1:{_httpPort};DIRECT;"); + var pacText = await File.ReadAllTextAsync(fileName); + pacText = pacText.Replace("__PROXY__", $"PROXY 127.0.0.1:{_httpPort};DIRECT;"); var sb = new StringBuilder(); sb.AppendLine("HTTP/1.0 200 OK"); diff --git a/v2rayN/ServiceLib/Manager/ProfileExManager.cs b/v2rayN/ServiceLib/Manager/ProfileExManager.cs index 0a3b7399..739bd550 100644 --- a/v2rayN/ServiceLib/Manager/ProfileExManager.cs +++ b/v2rayN/ServiceLib/Manager/ProfileExManager.cs @@ -1,5 +1,3 @@ -using System.Collections.Concurrent; - //using System.Reactive.Linq; namespace ServiceLib.Manager; diff --git a/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs new file mode 100644 index 00000000..499ad3de --- /dev/null +++ b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs @@ -0,0 +1,320 @@ +namespace ServiceLib.Manager; + +public class ProfileGroupItemManager +{ + private static readonly Lazy _instance = new(() => new()); + private ConcurrentDictionary _items = new(); + + public static ProfileGroupItemManager Instance => _instance.Value; + private static readonly string _tag = "ProfileGroupItemManager"; + + private ProfileGroupItemManager() + { + } + + public async Task Init() + { + await InitData(); + } + + // Read-only getters: do not create or mark dirty + public bool TryGet(string indexId, out ProfileGroupItem? item) + { + item = null; + if (string.IsNullOrWhiteSpace(indexId)) + { + return false; + } + + return _items.TryGetValue(indexId, out item); + } + + public ProfileGroupItem? GetOrDefault(string indexId) + { + return string.IsNullOrWhiteSpace(indexId) ? null : (_items.TryGetValue(indexId, out var v) ? v : null); + } + + private async Task InitData() + { + await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileGroupItem where IndexId not in ( select indexId from ProfileItem )"); + + var list = await SQLiteHelper.Instance.TableAsync().ToListAsync(); + _items = new ConcurrentDictionary(list.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!)); + } + + private ProfileGroupItem AddProfileGroupItem(string indexId) + { + var profileGroupItem = new ProfileGroupItem() + { + IndexId = indexId, + ChildItems = string.Empty, + MultipleLoad = EMultipleLoad.LeastPing + }; + + _items[indexId] = profileGroupItem; + return profileGroupItem; + } + + private ProfileGroupItem GetProfileGroupItem(string indexId) + { + if (string.IsNullOrEmpty(indexId)) + { + indexId = Utils.GetGuid(false); + } + + return _items.GetOrAdd(indexId, AddProfileGroupItem); + } + + public async Task ClearAll() + { + await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileGroupItem "); + _items.Clear(); + } + + public async Task SaveTo() + { + try + { + var lstExists = await SQLiteHelper.Instance.TableAsync().ToListAsync(); + var existsMap = lstExists.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!); + + var lstInserts = new List(); + var lstUpdates = new List(); + + foreach (var item in _items.Values) + { + if (string.IsNullOrEmpty(item.IndexId)) + { + continue; + } + + if (existsMap.ContainsKey(item.IndexId)) + { + lstUpdates.Add(item); + } + else + { + lstInserts.Add(item); + } + } + + try + { + if (lstInserts.Count > 0) + { + await SQLiteHelper.Instance.InsertAllAsync(lstInserts); + } + + if (lstUpdates.Count > 0) + { + await SQLiteHelper.Instance.UpdateAllAsync(lstUpdates); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + + public ProfileGroupItem GetOrCreateAndMarkDirty(string indexId) + { + return GetProfileGroupItem(indexId); + } + + public async ValueTask DisposeAsync() + { + await SaveTo(); + } + + public async Task SaveItemAsync(ProfileGroupItem item) + { + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + + if (string.IsNullOrWhiteSpace(item.IndexId)) + { + throw new ArgumentException("IndexId required", nameof(item)); + } + + _items[item.IndexId] = item; + + try + { + var lst = await SQLiteHelper.Instance.TableAsync().Where(t => t.IndexId == item.IndexId).ToListAsync(); + if (lst != null && lst.Count > 0) + { + await SQLiteHelper.Instance.UpdateAllAsync(new List { item }); + } + else + { + await SQLiteHelper.Instance.InsertAllAsync(new List { item }); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + + #region Helper + + public static bool HasCycle(string? indexId) + { + return HasCycle(indexId, new HashSet(), new HashSet()); + } + + public static bool HasCycle(string? indexId, HashSet visited, HashSet stack) + { + if (indexId.IsNullOrEmpty()) + { + return false; + } + + if (stack.Contains(indexId)) + { + return true; + } + + if (visited.Contains(indexId)) + { + return false; + } + + visited.Add(indexId); + stack.Add(indexId); + + try + { + Instance.TryGet(indexId, out var groupItem); + + if (groupItem == null || groupItem.ChildItems.IsNullOrEmpty()) + { + return false; + } + + var childIds = Utils.String2List(groupItem.ChildItems) + .Where(p => !string.IsNullOrEmpty(p)) + .ToList(); + if (childIds == null) + { + return false; + } + + foreach (var child in childIds) + { + if (HasCycle(child, visited, stack)) + { + return true; + } + } + + return false; + } + finally + { + stack.Remove(indexId); + } + } + + public static async Task<(List Items, ProfileGroupItem? Group)> GetChildProfileItems(string? indexId) + { + Instance.TryGet(indexId, out var profileGroupItem); + if (profileGroupItem == null || profileGroupItem.NotHasChild()) + { + return (new List(), profileGroupItem); + } + var items = await GetChildProfileItems(profileGroupItem); + var subItems = await GetSubChildProfileItems(profileGroupItem); + items.AddRange(subItems); + + return (items, profileGroupItem); + } + + public static async Task> GetChildProfileItems(ProfileGroupItem? group) + { + if (group == null || group.ChildItems.IsNullOrEmpty()) + { + return new(); + } + var childProfiles = (await Task.WhenAll( + Utils.String2List(group.ChildItems) + .Where(p => !p.IsNullOrEmpty()) + .Select(AppManager.Instance.GetProfileItem) + )) + .Where(p => + p != null && + p.IsValid() && + p.ConfigType != EConfigType.Custom + ) + .ToList(); + return childProfiles; + } + + public static async Task> GetSubChildProfileItems(ProfileGroupItem? group) + { + if (group == null || group.SubChildItems.IsNullOrEmpty()) + { + return new(); + } + var childProfiles = await AppManager.Instance.ProfileItems(group.SubChildItems); + + return childProfiles.Where(p => + p != null && + p.IsValid() && + !p.ConfigType.IsComplexType() && + (group.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, group.Filter)) + ) + .ToList(); + } + + public static async Task> GetAllChildDomainAddresses(string indexId) + { + // include grand children + var childAddresses = new HashSet(); + if (!Instance.TryGet(indexId, out var groupItem) || groupItem == null) + { + return childAddresses; + } + + if (groupItem.SubChildItems.IsNotEmpty()) + { + var subItems = await GetSubChildProfileItems(groupItem); + subItems.ForEach(p => childAddresses.Add(p.Address)); + } + + var childIds = Utils.String2List(groupItem.ChildItems) ?? []; + + foreach (var childId in childIds) + { + var childNode = await AppManager.Instance.GetProfileItem(childId); + if (childNode == null) + { + continue; + } + + if (!childNode.IsComplex()) + { + childAddresses.Add(childNode.Address); + } + else if (childNode.ConfigType.IsGroupType()) + { + var subAddresses = await GetAllChildDomainAddresses(childNode.IndexId); + foreach (var addr in subAddresses) + { + childAddresses.Add(addr); + } + } + } + + return childAddresses; + } + + #endregion Helper +} diff --git a/v2rayN/ServiceLib/Manager/TaskManager.cs b/v2rayN/ServiceLib/Manager/TaskManager.cs index dfbd242d..d1c73157 100644 --- a/v2rayN/ServiceLib/Manager/TaskManager.cs +++ b/v2rayN/ServiceLib/Manager/TaskManager.cs @@ -26,15 +26,29 @@ public class TaskManager await Task.Delay(1000 * 60); //Execute once 1 minute - await UpdateTaskRunSubscription(); + try + { + await UpdateTaskRunSubscription(); + } + catch (Exception ex) + { + Logging.SaveLog("ScheduledTasks - UpdateTaskRunSubscription", ex); + } //Execute once 20 minute if (numOfExecuted % 20 == 0) { //Logging.SaveLog("Execute save config"); - await ConfigHandler.SaveConfig(_config); - await ProfileExManager.Instance.SaveTo(); + try + { + await ConfigHandler.SaveConfig(_config); + await ProfileExManager.Instance.SaveTo(); + } + catch (Exception ex) + { + Logging.SaveLog("ScheduledTasks - SaveConfig", ex); + } } //Execute once 1 hour @@ -42,12 +56,18 @@ public class TaskManager { //Logging.SaveLog("Execute delete expired files"); - FileManager.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1)); - FileManager.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1)); - FileManager.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1)); + FileUtils.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1)); + FileUtils.DeleteExpiredFiles(Utils.GetLogPath(), DateTime.Now.AddMonths(-1)); + FileUtils.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1)); - //Check once 1 hour - await UpdateTaskRunGeo(numOfExecuted / 60); + try + { + await UpdateTaskRunGeo(numOfExecuted / 60); + } + catch (Exception ex) + { + Logging.SaveLog("ScheduledTasks - UpdateTaskRunGeo", ex); + } } numOfExecuted++; @@ -91,11 +111,10 @@ public class TaskManager { Logging.SaveLog("Execute update geo files"); - var updateHandle = new UpdateService(); - await updateHandle.UpdateGeoFileAll(_config, async (success, msg) => + await new UpdateService(_config, async (success, msg) => { await _updateFunc?.Invoke(false, msg); - }); + }).UpdateGeoFileAll(); } } } diff --git a/v2rayN/ServiceLib/Manager/WebDavManager.cs b/v2rayN/ServiceLib/Manager/WebDavManager.cs index 3f5c9ea3..83ae24ce 100644 --- a/v2rayN/ServiceLib/Manager/WebDavManager.cs +++ b/v2rayN/ServiceLib/Manager/WebDavManager.cs @@ -1,4 +1,3 @@ -using System.Net; using WebDav; namespace ServiceLib.Manager; diff --git a/v2rayN/ServiceLib/Models/CheckUpdateModel.cs b/v2rayN/ServiceLib/Models/CheckUpdateModel.cs index f06eedc9..2707cc6e 100644 --- a/v2rayN/ServiceLib/Models/CheckUpdateModel.cs +++ b/v2rayN/ServiceLib/Models/CheckUpdateModel.cs @@ -1,6 +1,3 @@ -using ReactiveUI; -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.Models; public class CheckUpdateModel : ReactiveObject diff --git a/v2rayN/ServiceLib/Models/ClashProxyModel.cs b/v2rayN/ServiceLib/Models/ClashProxyModel.cs index 014b0f12..10d68e14 100644 --- a/v2rayN/ServiceLib/Models/ClashProxyModel.cs +++ b/v2rayN/ServiceLib/Models/ClashProxyModel.cs @@ -1,6 +1,3 @@ -using ReactiveUI; -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.Models; [Serializable] diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs index ef369990..9a0cff43 100644 --- a/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/ConfigItems.cs @@ -71,7 +71,6 @@ public class GUIItem public bool DisplayRealTimeSpeed { get; set; } = true; public bool KeepOlderDedupl { get; set; } public int AutoUpdateInterval { get; set; } - public bool EnableSecurityProtocolTls13 { get; set; } public int TrayMenuServersLimit { get; set; } = 20; public bool EnableHWA { get; set; } = false; public bool EnableLog { get; set; } = true; @@ -220,6 +219,8 @@ public class SystemProxyItem public string SystemProxyExceptions { get; set; } public bool NotProxyLocalAddress { get; set; } = true; public string SystemProxyAdvancedProtocol { get; set; } + public string? CustomSystemProxyPacPath { get; set; } + public string? CustomSystemProxyScriptPath { get; set; } } [Serializable] @@ -264,8 +265,7 @@ public class SimpleDNSItem public bool? BlockBindingQuery { get; set; } public string? DirectDNS { get; set; } public string? RemoteDNS { get; set; } - public string? SingboxOutboundsResolveDNS { get; set; } - public string? SingboxFinalResolveDNS { get; set; } + public string? BootstrapDNS { get; set; } public string? RayStrategy4Freedom { get; set; } public string? SingboxStrategy4Direct { get; set; } public string? SingboxStrategy4Proxy { get; set; } diff --git a/v2rayN/ServiceLib/Models/DNSItem.cs b/v2rayN/ServiceLib/Models/DNSItem.cs index 9474d906..2dea42d0 100644 --- a/v2rayN/ServiceLib/Models/DNSItem.cs +++ b/v2rayN/ServiceLib/Models/DNSItem.cs @@ -1,5 +1,3 @@ -using SQLite; - namespace ServiceLib.Models; [Serializable] diff --git a/v2rayN/ServiceLib/Models/FullConfigTemplateItem.cs b/v2rayN/ServiceLib/Models/FullConfigTemplateItem.cs index f3881325..b3e3b14e 100644 --- a/v2rayN/ServiceLib/Models/FullConfigTemplateItem.cs +++ b/v2rayN/ServiceLib/Models/FullConfigTemplateItem.cs @@ -1,5 +1,3 @@ -using SQLite; - namespace ServiceLib.Models; [Serializable] diff --git a/v2rayN/ServiceLib/Models/GitHubRelease.cs b/v2rayN/ServiceLib/Models/GitHubRelease.cs index 9875c0a0..f6549467 100644 --- a/v2rayN/ServiceLib/Models/GitHubRelease.cs +++ b/v2rayN/ServiceLib/Models/GitHubRelease.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace ServiceLib.Models; public class GitHubReleaseAsset diff --git a/v2rayN/ServiceLib/Models/ProfileExItem.cs b/v2rayN/ServiceLib/Models/ProfileExItem.cs index 49a85749..33b20c57 100644 --- a/v2rayN/ServiceLib/Models/ProfileExItem.cs +++ b/v2rayN/ServiceLib/Models/ProfileExItem.cs @@ -1,5 +1,3 @@ -using SQLite; - namespace ServiceLib.Models; [Serializable] diff --git a/v2rayN/ServiceLib/Models/ProfileGroupItem.cs b/v2rayN/ServiceLib/Models/ProfileGroupItem.cs new file mode 100644 index 00000000..12c0f899 --- /dev/null +++ b/v2rayN/ServiceLib/Models/ProfileGroupItem.cs @@ -0,0 +1,21 @@ +namespace ServiceLib.Models; + +[Serializable] +public class ProfileGroupItem +{ + [PrimaryKey] + public string IndexId { get; set; } + + public string ChildItems { get; set; } + + public string? SubChildItems { get; set; } + + public string? Filter { get; set; } + + public EMultipleLoad MultipleLoad { get; set; } = EMultipleLoad.LeastPing; + + public bool NotHasChild() + { + return string.IsNullOrWhiteSpace(ChildItems) && string.IsNullOrWhiteSpace(SubChildItems); + } +} diff --git a/v2rayN/ServiceLib/Models/ProfileItem.cs b/v2rayN/ServiceLib/Models/ProfileItem.cs index 998aa120..00ed38da 100644 --- a/v2rayN/ServiceLib/Models/ProfileItem.cs +++ b/v2rayN/ServiceLib/Models/ProfileItem.cs @@ -1,6 +1,3 @@ -using ReactiveUI; -using SQLite; - namespace ServiceLib.Models; [Serializable] @@ -31,19 +28,22 @@ public class ProfileItem : ReactiveObject public string GetSummary() { - var summary = $"[{(ConfigType).ToString()}] "; - var arrAddr = Address.Contains(':') ? Address.Split(':') : Address.Split('.'); - var addr = arrAddr.Length switch + var summary = $"[{ConfigType.ToString()}] "; + if (IsComplex()) { - > 2 => $"{arrAddr.First()}***{arrAddr.Last()}", - > 1 => $"***{arrAddr.Last()}", - _ => Address - }; - summary += ConfigType switch + summary += $"[{CoreType.ToString()}]{Remarks}"; + } + else { - EConfigType.Custom => $"[{CoreType.ToString()}]{Remarks}", - _ => $"{Remarks}({addr}:{Port})" - }; + var arrAddr = Address.Contains(':') ? Address.Split(':') : Address.Split('.'); + var addr = arrAddr.Length switch + { + > 2 => $"{arrAddr.First()}***{arrAddr.Last()}", + > 1 => $"***{arrAddr.Last()}", + _ => Address + }; + summary += $"{Remarks}({addr}:{Port})"; + } return summary; } @@ -61,6 +61,70 @@ public class ProfileItem : ReactiveObject return Network.TrimEx(); } + public bool IsComplex() + { + return ConfigType.IsComplexType(); + } + + public bool IsValid() + { + if (IsComplex()) + { + return true; + } + + if (Address.IsNullOrEmpty() || Port is <= 0 or >= 65536) + { + return false; + } + + switch (ConfigType) + { + case EConfigType.VMess: + if (Id.IsNullOrEmpty() || !Utils.IsGuidByParse(Id)) + { + return false; + } + + break; + + case EConfigType.VLESS: + if (Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(Id) && Id.Length > 30)) + { + return false; + } + + if (!Global.Flows.Contains(Flow)) + { + return false; + } + + break; + + case EConfigType.Shadowsocks: + if (Id.IsNullOrEmpty()) + { + return false; + } + + if (string.IsNullOrEmpty(Security) || !Global.SsSecuritiesInSingbox.Contains(Security)) + { + return false; + } + + break; + } + + if ((ConfigType is EConfigType.VLESS or EConfigType.Trojan) + && StreamSecurity == Global.StreamSecurityReality + && PublicKey.IsNullOrEmpty()) + { + return false; + } + + return true; + } + #endregion function [PrimaryKey] @@ -96,4 +160,5 @@ public class ProfileItem : ReactiveObject public string Mldsa65Verify { get; set; } public string Extra { get; set; } public bool? MuxEnabled { get; set; } + public string Cert { get; set; } } diff --git a/v2rayN/ServiceLib/Models/ProfileItemModel.cs b/v2rayN/ServiceLib/Models/ProfileItemModel.cs index 40ba3f1d..b4d72119 100644 --- a/v2rayN/ServiceLib/Models/ProfileItemModel.cs +++ b/v2rayN/ServiceLib/Models/ProfileItemModel.cs @@ -1,5 +1,3 @@ -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.Models; [Serializable] diff --git a/v2rayN/ServiceLib/Models/RoutingItem.cs b/v2rayN/ServiceLib/Models/RoutingItem.cs index 78d93445..ddd27a9b 100644 --- a/v2rayN/ServiceLib/Models/RoutingItem.cs +++ b/v2rayN/ServiceLib/Models/RoutingItem.cs @@ -1,5 +1,3 @@ -using SQLite; - namespace ServiceLib.Models; [Serializable] diff --git a/v2rayN/ServiceLib/Models/RulesItem.cs b/v2rayN/ServiceLib/Models/RulesItem.cs index 2aedae08..5a5cf529 100644 --- a/v2rayN/ServiceLib/Models/RulesItem.cs +++ b/v2rayN/ServiceLib/Models/RulesItem.cs @@ -15,4 +15,5 @@ public class RulesItem public List? Process { get; set; } public bool Enabled { get; set; } = true; public string? Remarks { get; set; } + public ERuleType? RuleType { get; set; } } diff --git a/v2rayN/ServiceLib/Models/RulesItemModel.cs b/v2rayN/ServiceLib/Models/RulesItemModel.cs index 973f2b84..f3eda16b 100644 --- a/v2rayN/ServiceLib/Models/RulesItemModel.cs +++ b/v2rayN/ServiceLib/Models/RulesItemModel.cs @@ -7,4 +7,5 @@ public class RulesItemModel : RulesItem public string Ips { get; set; } public string Domains { get; set; } public string Protocols { get; set; } + public string RuleTypeName { get; set; } } diff --git a/v2rayN/ServiceLib/Common/SemanticVersion.cs b/v2rayN/ServiceLib/Models/SemanticVersion.cs similarity index 99% rename from v2rayN/ServiceLib/Common/SemanticVersion.cs rename to v2rayN/ServiceLib/Models/SemanticVersion.cs index 64167084..78463434 100644 --- a/v2rayN/ServiceLib/Common/SemanticVersion.cs +++ b/v2rayN/ServiceLib/Models/SemanticVersion.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Common; +namespace ServiceLib.Models; public class SemanticVersion { diff --git a/v2rayN/ServiceLib/Models/ServerStatItem.cs b/v2rayN/ServiceLib/Models/ServerStatItem.cs index b5008465..05cd1ee6 100644 --- a/v2rayN/ServiceLib/Models/ServerStatItem.cs +++ b/v2rayN/ServiceLib/Models/ServerStatItem.cs @@ -1,5 +1,3 @@ -using SQLite; - namespace ServiceLib.Models; [Serializable] diff --git a/v2rayN/ServiceLib/Models/SingboxConfig.cs b/v2rayN/ServiceLib/Models/SingboxConfig.cs index a5eec4ae..6dde5e7c 100644 --- a/v2rayN/ServiceLib/Models/SingboxConfig.cs +++ b/v2rayN/ServiceLib/Models/SingboxConfig.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace ServiceLib.Models; public class SingboxConfig @@ -145,6 +143,7 @@ public class Outbound4Sbox : BaseServer4Sbox public string? plugin_opts { get; set; } public List? outbounds { get; set; } public bool? interrupt_exist_connections { get; set; } + public int? tolerance { get; set; } } public class Endpoints4Sbox : BaseServer4Sbox @@ -182,6 +181,7 @@ public class Tls4Sbox public bool? fragment { get; set; } public string? fragment_fallback_delay { get; set; } public bool? record_fragment { get; set; } + public List? certificate { get; set; } } public class Multiplex4Sbox diff --git a/v2rayN/ServiceLib/Models/SubItem.cs b/v2rayN/ServiceLib/Models/SubItem.cs index cc1fb518..612ec15b 100644 --- a/v2rayN/ServiceLib/Models/SubItem.cs +++ b/v2rayN/ServiceLib/Models/SubItem.cs @@ -1,5 +1,3 @@ -using SQLite; - namespace ServiceLib.Models; [Serializable] diff --git a/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayN/ServiceLib/Models/V2rayConfig.cs index cff3cf8b..e10cd0d9 100644 --- a/v2rayN/ServiceLib/Models/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/V2rayConfig.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace ServiceLib.Models; public class V2rayConfig @@ -217,6 +215,7 @@ public class Dns4Ray public class DnsServer4Ray { public string? address { get; set; } + public int? port { get; set; } public List? domains { get; set; } public bool? skipFallback { get; set; } public List? expectedIPs { get; set; } @@ -355,6 +354,14 @@ public class TlsSettings4Ray public string? shortId { get; set; } public string? spiderX { get; set; } public string? mldsa65Verify { get; set; } + public List? certificates { get; set; } + public bool? disableSystemRoot { get; set; } +} + +public class CertificateSettings4Ray +{ + public List? certificate { get; set; } + public string? usage { get; set; } } public class TcpSettings4Ray diff --git a/v2rayN/ServiceLib/Models/VmessQRCode.cs b/v2rayN/ServiceLib/Models/VmessQRCode.cs index c51d9986..f182c328 100644 --- a/v2rayN/ServiceLib/Models/VmessQRCode.cs +++ b/v2rayN/ServiceLib/Models/VmessQRCode.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace ServiceLib.Models; /// @@ -40,4 +38,6 @@ public class VmessQRCode public string alpn { get; set; } = string.Empty; public string fp { get; set; } = string.Empty; + + public string insecure { get; set; } = string.Empty; } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index afa9ef49..72a0e4bf 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -87,6 +87,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Certificate not set 的本地化字符串。 + /// + public static string CertNotSet { + get { + return ResourceManager.GetString("CertNotSet", resourceCulture); + } + } + + /// + /// 查找类似 Certificate set 的本地化字符串。 + /// + public static string CertSet { + get { + return ResourceManager.GetString("CertSet", resourceCulture); + } + } + /// /// 查找类似 Please check the Configuration settings first. 的本地化字符串。 /// @@ -114,6 +132,33 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Core '{0}' does not support network type '{1}'. 的本地化字符串。 + /// + public static string CoreNotSupportNetwork { + get { + return ResourceManager.GetString("CoreNotSupportNetwork", resourceCulture); + } + } + + /// + /// 查找类似 Core '{0}' does not support protocol '{1}'. 的本地化字符串。 + /// + public static string CoreNotSupportProtocol { + get { + return ResourceManager.GetString("CoreNotSupportProtocol", resourceCulture); + } + } + + /// + /// 查找类似 Core '{0}' does not support protocol '{1}' when using transport '{2}'. 的本地化字符串。 + /// + public static string CoreNotSupportProtocolTransport { + get { + return ResourceManager.GetString("CoreNotSupportProtocolTransport", resourceCulture); + } + } + /// /// 查找类似 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. 的本地化字符串。 /// @@ -267,6 +312,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Group '{0}' is empty. Please add at least one node. 的本地化字符串。 + /// + public static string GroupEmpty { + get { + return ResourceManager.GetString("GroupEmpty", resourceCulture); + } + } + + /// + /// 查找类似 {0} Group cannot reference itself or have a circular reference 的本地化字符串。 + /// + public static string GroupSelfReference { + get { + return ResourceManager.GetString("GroupSelfReference", resourceCulture); + } + } + /// /// 查找类似 This is not the correct configuration, please check 的本地化字符串。 /// @@ -294,6 +357,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 The {0} property is invalid, please check. 的本地化字符串。 + /// + public static string InvalidProperty { + get { + return ResourceManager.GetString("InvalidProperty", resourceCulture); + } + } + /// /// 查找类似 Invalid address (URL) 的本地化字符串。 /// @@ -538,7 +610,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Speed (M/s) 的本地化字符串。 + /// 查找类似 Speed (MB/s) 的本地化字符串。 /// public static string LvTestSpeed { get { @@ -672,6 +744,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Add Child Configuration 的本地化字符串。 + /// + public static string menuAddChildServer { + get { + return ResourceManager.GetString("menuAddChildServer", resourceCulture); + } + } + /// /// 查找类似 Add a custom configuration Configuration 的本地化字符串。 /// @@ -699,6 +780,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Add Policy Group Configuration 的本地化字符串。 + /// + public static string menuAddPolicyGroupServer { + get { + return ResourceManager.GetString("menuAddPolicyGroupServer", resourceCulture); + } + } + + /// + /// 查找类似 Add Proxy Chain Configuration 的本地化字符串。 + /// + public static string menuAddProxyChainServer { + get { + return ResourceManager.GetString("menuAddProxyChainServer", resourceCulture); + } + } + /// /// 查找类似 Import Share Links from clipboard (Ctrl+V) 的本地化字符串。 /// @@ -942,6 +1041,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Test real delay 的本地化字符串。 + /// + public static string menuFastRealPing { + get { + return ResourceManager.GetString("menuFastRealPing", resourceCulture); + } + } + /// /// 查找类似 Full Config Template Setting 的本地化字符串。 /// @@ -951,6 +1059,78 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Generate Policy Group from Multiple Profiles 的本地化字符串。 + /// + public static string menuGenGroupMultipleServer { + get { + return ResourceManager.GetString("menuGenGroupMultipleServer", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration Fallback by sing-box 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerSingBoxFallback { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxFallback", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration LeastPing by sing-box 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerSingBoxLeastPing { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxLeastPing", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration Fallback by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayFallback { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayFallback", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration LeastLoad by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayLeastLoad { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastLoad", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration LeastPing by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayLeastPing { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastPing", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration Random by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayRandom { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayRandom", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration RoundRobin by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayRoundRobin { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayRoundRobin", resourceCulture); + } + } + /// /// 查找类似 Global Hotkey Setting 的本地化字符串。 /// @@ -1311,6 +1491,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Remove Child Configuration 的本地化字符串。 + /// + public static string menuRemoveChildServer { + get { + return ResourceManager.GetString("menuRemoveChildServer", resourceCulture); + } + } + /// /// 查找类似 Remove duplicate Configurations 的本地化字符串。 /// @@ -1464,6 +1653,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Configuration List 的本地化字符串。 + /// + public static string menuServerList { + get { + return ResourceManager.GetString("menuServerList", resourceCulture); + } + } + /// /// 查找类似 Configurations 的本地化字符串。 /// @@ -1473,60 +1671,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Multi-Configuration to custom configuration 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServer { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServer", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration LeastPing by sing-box 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerSingBoxLeastPing { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerSingBoxLeastPing", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration LeastLoad by Xray 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerXrayLeastLoad { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerXrayLeastLoad", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration LeastPing by Xray 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerXrayLeastPing { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerXrayLeastPing", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration Random by Xray 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerXrayRandom { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerXrayRandom", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration RoundRobin by Xray 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerXrayRoundRobin { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerXrayRoundRobin", resourceCulture); - } - } - /// /// 查找类似 Set as active Configuration (Enter) 的本地化字符串。 /// @@ -1932,6 +2076,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Node alias '{0}' does not exist. 的本地化字符串。 + /// + public static string NodeTagNotExist { + get { + return ResourceManager.GetString("NodeTagNotExist", resourceCulture); + } + } + /// /// 查找类似 Non-VMess or SS protocol 的本地化字符串。 /// @@ -1959,6 +2112,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Not support protocol '{0}'. 的本地化字符串。 + /// + public static string NotSupportProtocol { + get { + return ResourceManager.GetString("NotSupportProtocol", resourceCulture); + } + } + /// /// 查找类似 Scan completed, no valid QR code found 的本地化字符串。 /// @@ -1986,6 +2148,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Please Add At Least One Configuration 的本地化字符串。 + /// + public static string PleaseAddAtLeastOneServer { + get { + return ResourceManager.GetString("PleaseAddAtLeastOneServer", resourceCulture); + } + } + /// /// 查找类似 Please fill Remarks 的本地化字符串。 /// @@ -2031,6 +2202,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Policy group: 的本地化字符串。 + /// + public static string PolicyGroupPrefix { + get { + return ResourceManager.GetString("PolicyGroupPrefix", resourceCulture); + } + } + + /// + /// 查找类似 Proxy chained: 的本地化字符串。 + /// + public static string ProxyChainedPrefix { + get { + return ResourceManager.GetString("ProxyChainedPrefix", resourceCulture); + } + } + /// /// 查找类似 Global hotkey {0} registration failed, reason: {1} 的本地化字符串。 /// @@ -2094,6 +2283,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Routing rule outbound: 的本地化字符串。 + /// + public static string RoutingRuleOutboundPrefix { + get { + return ResourceManager.GetString("RoutingRuleOutboundPrefix", resourceCulture); + } + } + /// /// 查找类似 Run as Admin 的本地化字符串。 /// @@ -2112,6 +2310,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Please set a valid domain 的本地化字符串。 + /// + public static string ServerNameMustBeValidDomain { + get { + return ResourceManager.GetString("ServerNameMustBeValidDomain", resourceCulture); + } + } + /// /// 查找类似 {0} : {1}/s↑ | {2}/s↓ 的本地化字符串。 /// @@ -2329,7 +2536,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Prevent domain-based routing rules from failing 的本地化字符串。 + /// 查找类似 Block ECH and HTTP/3 availability checks when enabled 的本地化字符串。 /// public static string TbBlockSVCBHTTPSQueriesTips { get { @@ -2337,6 +2544,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Bootstrap DNS 的本地化字符串。 + /// + public static string TbBootstrapDNS { + get { + return ResourceManager.GetString("TbBootstrapDNS", resourceCulture); + } + } + + /// + /// 查找类似 Resolve DNS server domains, requires IP 的本地化字符串。 + /// + public static string TbBootstrapDNSTips { + get { + return ResourceManager.GetString("TbBootstrapDNSTips", resourceCulture); + } + } + /// /// 查找类似 Browse 的本地化字符串。 /// @@ -2355,6 +2580,27 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Certificate Pinning 的本地化字符串。 + /// + public static string TbCertPinning { + get { + return ResourceManager.GetString("TbCertPinning", resourceCulture); + } + } + + /// + /// 查找类似 Server Certificate (PEM format, optional) + ///When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. + /// + ///The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. 的本地化字符串。 + /// + public static string TbCertPinningTips { + get { + return ResourceManager.GetString("TbCertPinningTips", resourceCulture); + } + } + /// /// 查找类似 Clear system proxy 的本地化字符串。 /// @@ -2364,6 +2610,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Policy Group 的本地化字符串。 + /// + public static string TbConfigTypePolicyGroup { + get { + return ResourceManager.GetString("TbConfigTypePolicyGroup", resourceCulture); + } + } + + /// + /// 查找类似 Proxy Chain 的本地化字符串。 + /// + public static string TbConfigTypeProxyChain { + get { + return ResourceManager.GetString("TbConfigTypeProxyChain", resourceCulture); + } + } + /// /// 查找类似 Confirm 的本地化字符串。 /// @@ -2409,6 +2673,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 V2ray Custom DNS 的本地化字符串。 + /// + public static string TbCustomDnsRay { + get { + return ResourceManager.GetString("TbCustomDnsRay", resourceCulture); + } + } + + /// + /// 查找类似 sing-box Custom DNS 的本地化字符串。 + /// + public static string TbCustomDnsSingbox { + get { + return ResourceManager.GetString("TbCustomDnsSingbox", resourceCulture); + } + } + /// /// 查找类似 Display GUI 的本地化字符串。 /// @@ -2517,6 +2799,33 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Fallback 的本地化字符串。 + /// + public static string TbFallback { + get { + return ResourceManager.GetString("TbFallback", resourceCulture); + } + } + + /// + /// 查找类似 Fetch Certificate 的本地化字符串。 + /// + public static string TbFetchCert { + get { + return ResourceManager.GetString("TbFetchCert", resourceCulture); + } + } + + /// + /// 查找类似 Fetch Certificate Chain 的本地化字符串。 + /// + public static string TbFetchCertChain { + get { + return ResourceManager.GetString("TbFetchCertChain", resourceCulture); + } + } + /// /// 查找类似 Fingerprint 的本地化字符串。 /// @@ -2634,6 +2943,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Most Stable 的本地化字符串。 + /// + public static string TbLeastLoad { + get { + return ResourceManager.GetString("TbLeastLoad", resourceCulture); + } + } + + /// + /// 查找类似 Lowest Latency 的本地化字符串。 + /// + public static string TbLeastPing { + get { + return ResourceManager.GetString("TbLeastPing", resourceCulture); + } + } + /// /// 查找类似 Address (IPv4, IPv6) 的本地化字符串。 /// @@ -2688,6 +3015,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Auto add filtered configuration from subscription groups 的本地化字符串。 + /// + public static string TbPolicyGroupSubChildTip { + get { + return ResourceManager.GetString("TbPolicyGroupSubChildTip", resourceCulture); + } + } + + /// + /// 查找类似 Policy Group Type 的本地化字符串。 + /// + public static string TbPolicyGroupType { + get { + return ResourceManager.GetString("TbPolicyGroupType", resourceCulture); + } + } + /// /// 查找类似 Port 的本地化字符串。 /// @@ -2760,6 +3105,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Random 的本地化字符串。 + /// + public static string TbRandom { + get { + return ResourceManager.GetString("TbRandom", resourceCulture); + } + } + /// /// 查找类似 v2ray Full Config Template 的本地化字符串。 /// @@ -2796,6 +3150,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Via proxy — please ensure remote availability 的本地化字符串。 + /// + public static string TbRemoteDNSTips { + get { + return ResourceManager.GetString("TbRemoteDNSTips", resourceCulture); + } + } + /// /// 查找类似 Camouflage domain(host) 的本地化字符串。 /// @@ -2823,6 +3186,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Round Robin 的本地化字符串。 + /// + public static string TbRoundRobin { + get { + return ResourceManager.GetString("TbRoundRobin", resourceCulture); + } + } + /// /// 查找类似 socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。 /// @@ -2904,6 +3276,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Rule Type 的本地化字符串。 + /// + public static string TbRuleType { + get { + return ResourceManager.GetString("TbRuleType", resourceCulture); + } + } + + /// + /// 查找类似 You can set separate rules for Routing and DNS, or select "ALL" to apply to both 的本地化字符串。 + /// + public static string TbRuleTypeTips { + get { + return ResourceManager.GetString("TbRuleTypeTips", resourceCulture); + } + } + /// /// 查找类似 sing-box Direct Resolution Strategy 的本地化字符串。 /// @@ -2913,33 +3303,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 The sing-box DoH resolution server can be overwritten 的本地化字符串。 - /// - public static string TbSBDoHOverride { - get { - return ResourceManager.GetString("TbSBDoHOverride", resourceCulture); - } - } - - /// - /// 查找类似 sing-box DoH Resolver Server 的本地化字符串。 - /// - public static string TbSBDoHResolverServer { - get { - return ResourceManager.GetString("TbSBDoHResolverServer", resourceCulture); - } - } - - /// - /// 查找类似 Fallback DNS Resolution, Suggest IP 的本地化字符串。 - /// - public static string TbSBFallbackDNSResolve { - get { - return ResourceManager.GetString("TbSBFallbackDNSResolve", resourceCulture); - } - } - /// /// 查找类似 sing-box Full Config Template 的本地化字符串。 /// @@ -2958,24 +3321,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Resolve Outbound Domains 的本地化字符串。 - /// - public static string TbSBOutboundDomainResolve { - get { - return ResourceManager.GetString("TbSBOutboundDomainResolve", resourceCulture); - } - } - - /// - /// 查找类似 Outbound DNS Resolution (sing-box) 的本地化字符串。 - /// - public static string TbSBOutboundsResolverDNS { - get { - return ResourceManager.GetString("TbSBOutboundsResolverDNS", resourceCulture); - } - } - /// /// 查找类似 sing-box Remote Resolution Strategy 的本地化字符串。 /// @@ -3111,24 +3456,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 V2ray DNS settings 的本地化字符串。 - /// - public static string TbSettingsCoreDns { - get { - return ResourceManager.GetString("TbSettingsCoreDns", resourceCulture); - } - } - - /// - /// 查找类似 sing-box DNS settings 的本地化字符串。 - /// - public static string TbSettingsCoreDnsSingbox { - get { - return ResourceManager.GetString("TbSettingsCoreDnsSingbox", resourceCulture); - } - } - /// /// 查找类似 Core: KCP settings 的本地化字符串。 /// @@ -3174,6 +3501,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Custom PAC file path 的本地化字符串。 + /// + public static string TbSettingsCustomSystemProxyPacPath { + get { + return ResourceManager.GetString("TbSettingsCustomSystemProxyPacPath", resourceCulture); + } + } + + /// + /// 查找类似 Custom system proxy script file path 的本地化字符串。 + /// + public static string TbSettingsCustomSystemProxyScriptPath { + get { + return ResourceManager.GetString("TbSettingsCustomSystemProxyScriptPath", resourceCulture); + } + } + /// /// 查找类似 Allow Insecure 的本地化字符串。 /// @@ -3408,6 +3753,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 If the system does not have a tray function, please do not enable it 的本地化字符串。 + /// + public static string TbSettingsHide2TrayWhenCloseTip { + get { + return ResourceManager.GetString("TbSettingsHide2TrayWhenCloseTip", resourceCulture); + } + } + /// /// 查找类似 Hysteria Max bandwidth (Up/Down) 的本地化字符串。 /// @@ -3489,6 +3843,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 macOS displays this in the Dock (requires restart) 的本地化字符串。 + /// + public static string TbSettingsMacOSShowInDock { + get { + return ResourceManager.GetString("TbSettingsMacOSShowInDock", resourceCulture); + } + } + /// /// 查找类似 Main layout orientation (requires restart) 的本地化字符串。 /// @@ -3723,15 +4086,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Enable Security Protocol TLS v1.3 (subscription/update) 的本地化字符串。 - /// - public static string TbSettingsTLS13 { - get { - return ResourceManager.GetString("TbSettingsTLS13", resourceCulture); - } - } - /// /// 查找类似 Tray right-click menu Configurations display limit 的本地化字符串。 /// @@ -4005,9 +4359,9 @@ namespace ServiceLib.Resx { /// /// 查找类似 xray Freedom Resolution Strategy 的本地化字符串。 /// - public static string TbXrayFreedomResolveStrategy { + public static string TbXrayFreedomStrategy { get { - return ResourceManager.GetString("TbXrayFreedomResolveStrategy", resourceCulture); + return ResourceManager.GetString("TbXrayFreedomStrategy", resourceCulture); } } diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 8537bbd6..6092cf69 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -672,8 +672,8 @@ هسته: تنظیمات اولیه - - تنظیمات V2ray DNS + + V2ray Custom DNS هسته: تنظیمات KCP @@ -744,9 +744,6 @@ تنظیمات پراکسی سیستم - - فعال کردن پروتکل امنیتی TLS نسخه 1.3 (اشتراک/به‌روزرسانی) - محدودیت نمایش سرورهای منوی سینی کلیک راست @@ -889,7 +886,7 @@ تاخیر (میلی‌ثانیه) - سرعت (M/s) + سرعت (MB/s) Core اجرا نشد، لطفاً گزارش را ببینید @@ -1008,8 +1005,8 @@ تنظیمات DNS - - تنظیمات DNS sing-box + + sing-box Custom DNS لطفا ساختار DNS را پر کنید، برای مشاهده سند کلیک کنید @@ -1374,22 +1371,22 @@ مخفی و پورت می شود، با کاما (،) جدا می شود - - چند سرور به پیکربندی سفارشی + + Generate Policy Group from Multiple Profiles - + چند سرور تصادفی توسط Xray - + چند سرور RoundRobin توسط Xray - + چند سرور LeastPing توسط Xray - + چند سرور LeastLoad توسط Xray - + LeastPing چند سرور توسط sing-box @@ -1416,19 +1413,10 @@ Domestic DNS - - Outbound DNS Resolution (sing-box) + + Via proxy — please ensure remote availability - - Resolve Outbound Domains - - - sing-box DoH Resolver Server - - - Fallback DNS Resolution, Suggest IP - - + xray Freedom Resolution Strategy @@ -1440,9 +1428,6 @@ Add Common DNS Hosts - - The sing-box DoH resolution server can be overwritten - FakeIP @@ -1471,7 +1456,7 @@ Custom DNS Enabled, This Page's Settings Invalid - Prevent domain-based routing rules from failing + Block ECH and HTTP/3 availability checks when enabled Please fill in the correct config template @@ -1512,4 +1497,139 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Configuration List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + {0} Group cannot reference itself or have a circular reference + + + Not support protocol '{0}'. + + + If the system does not have a tray function, please do not enable it + + + Rule Type + + + You can set separate rules for Routing and DNS, or select "ALL" to apply to both + + + Bootstrap DNS + + + Resolve DNS server domains, requires IP + + + Test real delay + + + Auto add filtered configuration from subscription groups + + + Certificate Pinning + + + Server Certificate (PEM format, optional) +When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. + +The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. + + + Fetch Certificate + + + Fetch Certificate Chain + + + Please set a valid domain + + + Certificate not set + + + Certificate set + + + Custom PAC file path + + + Custom system proxy script file path + + + macOS displays this in the Dock (requires restart) + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx new file mode 100644 index 00000000..50a488e0 --- /dev/null +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -0,0 +1,1635 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Lien de partage exporté vers le presse-papiers avec succès + + + Veuillez vérifier d’abord la config + + + Format de configuration incorrect + + + Attention : la configuration personnalisée dépend entièrement de vos paramètres et peut ne pas activer toutes les fonctions. Pour utiliser le proxy système, changez le port d’écoute manuellement. + + + Téléchargement commencé... + + + Échec de la conversion de la configuration + + + Échec de la génération du fichier de configuration par défaut + + + Échec de l’obtention de la configuration par défaut + + + Échec de l’importation de la configuration personnalisée + + + Échec de la lecture de la configuration + + + Veuillez saisir un port au format correct + + + Veuillez saisir le port d’écoute local + + + Veuillez saisir le mot de passe + + + Veuillez saisir l’adresse + + + Veuillez saisir l’ID utilisateur + + + Configuration incorrecte, veuillez vérifier + + + Initialiser la configuration + + + {0} {1} est déjà à jour. + + + {0} {1} est déjà à jour. + + + Adresse + + + Méthode de chiffrement + + + Port + + + Type + + + Grp abonn. + + + Téléchargement du jour + + + Téléversement du jour + + + Total téléchargé + + + Total téléversé + + + Protoc. Transp. + + + Téléchargement du Core réussi + + + Échec de l’importation du contenu d’abonnement + + + Contenu d’abonnement récupéré avec succès + + + Aucun abonnement valide défini + + + Analyse de {0} réussie + + + Début de la récupération du contenu d’abonnement + + + Début de la mise à jour de {0}... + + + Contenu d’abonnement invalide + + + Décompression en cours...... + + + Fin de la mise à jour des abonnements + + + Début de la mise à jour des abonnements + + + Mise à jour du Core réussie + + + Mise à jour du Core réussie ! Redémarrage du service... + + + Protocole non VMess ni SS + + + Fichier Core introuvable dans le dossier ({0}) (nom de fichier : {1}). Veuillez le télécharger puis le placer dans le dossier. Adresse de téléchargement : {2} + + + Analyse terminée, aucun QR code valide trouvé + + + Échec de l’opération, veuillez vérifier et réessayer + + + Veuillez saisir un alias + + + Veuillez sélectionner une méthode de chiffrement + + + Veuillez sélectionner un protocole + + + Sélectionnez d’abord une configuration + + + Déduplication des configurations terminée. Quantité initiale : {0}, quantité actuelle : {1}. + + + Confirmer la suppression ? + + + Le fichier de configuration client est enregistré dans : {0} + + + Démarrage du service ({0})... + + + Configuration réussie. {0} + + + Configuration personnalisée importée avec succès + + + {0} configurations importées depuis le presse-papiers + + + Lien de partage importé par analyse avec succès + + + Latence actuelle : {0} ms, {1} + + + Opération réussie + + + Veuillez d’abord sélectionner une règle + + + Confirmer la suppression de la règle ? + + + {0}, au moins un champ est obligatoire. + + + Alias + + + Adresse optionnelle (URL) + + + Quantité + + + Veuillez saisir l’URL + + + Ajouter les règles ? Choisir "Oui" pour ajouter, "Non" pour tout remplacer. + + + Téléchargement du fichier Geo : {0} réussi + + + Information + + + Icône personnalisée + + + Veuillez saisir un DNS personnalisé valide + + + *chemin ws/httpupgrade/xhttp + + + *chemin h2 + + + *clé de chiffrement QUIC + + + *nom de service gRPC + + + *hôte http, séparés par des virgules (,) + + + *hôte ws/httpupgrade/xhttp + + + *hôte h2, séparés par des virgules (,) + + + *méthode de chiffrement QUIC + + + *type de camouflage tcp + + + *type de camouflage kcp + + + *type de camouflage QUIC + + + *mode gRPC + + + TLS + + + *graine Kcp + + + Échec de l’enregistrement du raccourci global {0}, raison : {1} + + + Raccourci global {0} enregistré avec succès + + + Tous + + + Parcourir pour importer la configuration + + + Test en cours... + + + Réseau local + + + Local + + + Filtre : Entrée pour exécuter + + + Rechercher mises à jour + + + Fermer + + + Quitter + + + Paramètres des raccourcis globaux + + + Aide + + + Paramètres config + + + Promotion + + + Redémarrer + + + Paramètres de routage + + + Fichiers de config + + + Paramètres + + + Mettre à jour l’abonnement actuel (sans proxy) + + + Mettre à jour l’abonnement actuel (via le proxy) + + + Groupe abonnements + + + Paramètres des groupes d’abonnement + + + Mettre à jour tous les abonnements (sans proxy) + + + Mettre à jour tous les abonnements (via le proxy) + + + Proxy système + + + Effacer le proxy système + + + Ne pas modifier le proxy système + + + Mode PAC + + + Configuration auto proxy système + + + Couleur + + + Langue (redémarrage requis) + + + Importer liens depuis le presse-papiers (Ctrl+V) + + + Scanner le QR code à l’écran (Ctrl+S) + + + Cloner la sélection + + + Supprimer les doublons + + + Supprimer la sélection (multi-sélection) (Delete) + + + Définir comme actif (Entrée) + + + Effacer toutes les statistiques de service + + + Tester la latence de connexion réelle (multi-sélect) (Ctrl+R) + + + Trier selon les résultats de test + + + Tester la vitesse (multi-sélection) (Ctrl+T) + + + Tester la latence Tcping (multi-sélection) (Ctrl+O) + + + Exporter la configuration complète sélectionnée + + + Exporter les liens de partage vers le presse-papiers (multi-sélection) (Ctrl+C) + + + Ajouter une configuration personnalisée + + + Ajouter [Shadowsocks] + + + Ajouter [SOCKS] + + + Ajouter [Trojan] + + + Ajouter [VLESS] + + + Ajouter [VMess] + + + Tout sélectionner (Ctrl+A) + + + Tout effacer + + + Copier (Ctrl+C) + + + Tout copier + + + Tout sélect (Ctrl+A) + + + Ajouter + + + Supprimer + + + Éditer + + + Partager + + + Activer MAJ + + + Tri + + + User-Agent (optionnel) + + + Annuler + + + OK + + + Méthode de transport sous-jacente (transport) + + + Adresse (address) + + + Ignorer la vérification du certificat (allowInsecure) + + + ALPN + + + ID supplémentaire (alterId) + + + Empreinte + + + Type de camouflage (type) + + + ID utilisateur (id) + + + Protocole (network) + + + Chemin (path) + + + Port (port) + + + Alias (remarks) + + + Domaine de camouflage (host) + + + Méthode de chiffrement (security) + + + SNI + + + Sécurité couche transport (TLS) + + + *tcp par défaut ; un mauvais choix bloque la connexion + + + Type de Core + + + Contrôle de flux (flow) + + + Générer + + + Mot de passe (password) + + + Mot de passe (optionnel) + + + ID utilisateur (id) + + + Mode chiffrement (encryption) + + + Nom d’utilisateur (optionnel) + + + Mode chiffrement (encryption) + + + Port Socks + + + *Valeur du port Socks (config perso, optionnelle). Si défini, Xray/sing-box (Tun) démarre un service Socks en amont supplémentaire pour fournir le routage sélectif et l’affichage de la vitesse. + + + Parcourir + + + Éditer + + + Paramètres avancés du proxy, choix du protocole (optionnel) + + + Autoriser les connexions depuis le LAN + + + Masquer la fenêtre au démarrage + + + Intervalle de mise à jour auto des fichiers Geo (heures) + + + Core : paramètres de base + + + DNS personnalisé v2ray + + + Core : paramètres KCP + + + Paramètres du type de Core + + + Ignorer la vérif. du certificat (Dangereux) + + + domainStrategy de Freedom (sortant) + + + Auto-ajuster la largeur des colonnes après maj. abonnements + + + Vérifier les mises à jour pré-version (à activer avec prudence) + + + Exceptions + + + Exceptions : pour les adresses commençant par les caractères ci-dessous, ne pas utiliser le proxy. Séparez par des points-virgules (;). + + + Afficher la vitesse en temps réel (redémarrage requis) + + + À la déduplication, garder l’élément au plus petit numéro + + + Activer les journaux + + + Niveau de journalisation + + + Activer Mux multiplex + + + Paramètres v2rayN + + + Mot de passe d’authentification + + + DNS perso (plusieurs configurables, séparés par virgules) + + + Lever la restriction de proxy en boucle locale pour les applications Win10 UWP + + + Activer le sniffing de trafic + + + Port d’écoute mixte local + + + Lancer au démarrage (peut échouer) + + + Activer les statistiques de trafic (redémarrage requis) + + + URL de conversion d’abonnement (optionnel) + + + Paramètres du proxy système + + + Limite du nombre de configurations affichées dans le menu du plateau + + + Activer l’UDP + + + Nom d’utilisateur d’authentification + + + Effacer le proxy système + + + Afficher l’interface principale + + + Paramètres des raccourcis globaux + + + Appuyez directement sur les touches pour définir ; prendra effet après redémarrage + + + Ne pas modifier le proxy système + + + Réinitialiser + + + Configurer automatiquement le proxy système + + + Mode PAC + + + Partager (Ctrl+F) + + + Routage + + + Exécuter sans droits administrateur + + + Exécuter en tant qu’administrateur + + + Déplacer tout en bas (B) + + + Descendre (D) + + + Déplacer tout en haut (T) + + + Monter (U) + + + Filtre (regex pris en charge) + + + Site officiel de {0} + + + Ajouter un jeu de règles + + + Importer 1-clic du jeu de règles + + + Suppr. règles sélectionnées (Delete) + + + Définir comme règles actives (Entrée) + + + Stratégie résolution domaine + + + Liste des jeux de règles prédef. + + + *Règles de routage définies, séparées par des virgules (,); remplacez la virgule dans les expressions régulières par <COMMA> + + + Importer règles du presse-papiers + + + Importer règles depuis fichier + + + Importer depuis URL d’abonnement + + + Paramètres du jeu de règles + + + Ajouter une règle + + + Exporter les règles sélectionnées vers le presse-papiers + + + Liste des règles + + + Supprimer les règles sélectionnées (Delete) + + + Paramètres détaillés des règles de routage + + + Tri automatique par domaine, IP et nom de processus lors de l’enregistrement + + + Documentation détaillée des règles + + + Saisie DnsObject prise en charge (format JSON), cliquer pour doc + + + Laissez vide pour les groupes ordinaires + + + Paramètres de routage modifiés + + + Paramètres du proxy système modifiés + + + Routage uniquement (routeOnly) + + + Ne pas utiliser le proxy pour les adresses locales (Intranet) + + + Tester latence et vitesse multithread en un clic (Ctrl+E) + + + Latence (ms) + + + Vitesse (Mo/s) + + + Échec d’exécution du Core, veuillez consulter les messages + + + Filtrage par alias (regex) + + + Afficher les logs + + + Activer Tun + + + Ouvrir un nouveau port pour le LAN + + + Paramètres du mode Tun + + + Déplacer vers un groupe d’abonnement + + + Activer le tri par glisser-déposer des configurations (redémarrage requis) + + + Actualisation automatique + + + Ignorer le test + + + Éditer (Ctrl+D) + + + Double-cliquer sur l’interface principale pour activer + + + Test terminé + + + Empreinte TLS par défaut (fingerprint) + + + Agent utilisateur (User-Agent) + + + Valable uniquement pour les protocoles tcp/http et ws + + + Police actuelle (redémarrage requis) + + + Copiez les fichiers de police TTF/TTC dans le dossier guiFonts, effet après redémarrage + + + Port PAC = +3 ; port API Xray = +4 ; port API mihomo = +5 ; + + + Définir cette option avec des privilèges administrateur pour obtenir les droits admin au démarrage + + + Taille de police + + + Valeur délai d'expiration test vitesse unique + + + URL du fichier de test de vitesse + + + Déplacer vers haut/bas + + + PublicKey + + + ShortId + + + SpiderX + + + Activer l’accélération matérielle (redémarrage requis) + + + En attente du test (appuyer sur Échap pour arrêter)... + + + Désactiver cette option si coupure anormale + + + Mise à jour désactivée, abonnement ignoré + + + Redémarrer en tant qu’administrateur + + + Adresses suppl. (URL), séparées par des virgules ; la conversion d’abonnement sera désactivée + + + Intervalle maj. auto (min) + + + Activer l’enregistrement des journaux dans un fichier + + + Type cible conv. d’abonnement + + + Laisser vide si aucune conversion n’est nécessaire + + + Paramètres DNS + + + DNS personnalisé sing-box + + + Saisissez la structure JSON DNS ; cliquez pour voir la doc. + + + Cliquez pour importer la config DNS par défaut + + + Stratégie résolution domaine (sing-box) + + + Protocole de multiplexage Mux (sing-box) + + + Nom complet du processus (mode Tun) + + + IP ou IP CIDR + + + Domaine + + + Ajouter [Hysteria2] + + + Bande passante maximale Hysteria (Up/Down) + + + Utiliser les hosts du système + + + Ajouter [TUIC] + + + Algo contrôle congestion + + + Alias de config du proxy amont + + + Alias de config du proxy aval + + + Assurez-vous que l’alias config existe et est unique + + + Routage automatique + + + Routage strict + + + Pile de protocoles + + + MTU + + + Activer un port d’écoute supplémentaire + + + Activer IPv6 + + + Ajouter [WireGuard] + + + PrivateKey + + + Reserved (2,3,4) + + + Address (IPv4,IPv6) + + + Mot de passe d’obfuscation (obfs password) + + + (Domaine ou IP ou nom de processus) avec Port et Protocole et InboundTag => OutboundTag + + + Défilement automatique vers la fin + + + Adresse de test de connexion réelle + + + Ne vérifier l’existence de l’alias qu’à la maj. des abonnements + + + Arrêt du test en cours... + + + *Autorité gRPC + + + Ajouter [HTTP] + + + En conflit avec le proxy amont de groupe + + + Activer le fragmentation (Fragment) + + + Activer le fichier de cache de sing-box (fichiers règles) + + + Set de règles sing-box perso + + + Opération réussie. Veuillez redémarrer l’app via le menu Paramètres. + + + Ouvrir l’emplacement du fichier + + + Tri + + + Chaîne de routage + + + Par défaut + + + Latence + + + Vitesse de téléchargement + + + Trafic téléchargé + + + Hôte + + + Nom + + + Réseau + + + Heure + + + Type + + + Vitesse d’envoi + + + Trafic envoyé + + + Connexions en cours + + + Fermer la connexion + + + Fermer toutes les connexions + + + Proxies actuels + + + Mode règle + + + Direct + + + Global + + + Suivre la configuration d’origine + + + Règle + + + Test de latence + + + Test de latence partiel actuel + + + Actualiser + + + Définir comme actif (Entrée) + + + Stratégie de résolution par défaut des sortants + + + Orientation mise en page principale (redémarrage requis) + + + Adresse de résolution de domaine pour sortants + + + Ajuster auto. la largeur des colonnes + + + Exporter les liens de partage en Base64 vers le presse-papiers (multi-sélection) + + + Exporter la configuration complète sélectionnée vers le presse-papiers + + + Afficher ou masquer l’interface principale + + + Port Socks config personnalisée + + + Sauvegarder et restaurer + + + Sauvegarder localement + + + Restaurer localement + + + Sauvegarder à distance (WebDAV) + + + Restaurer à distance (WebDAV) + + + Local + + + Distant (WebDAV) + + + Adresse du serveur WebDAV + + + Compte WebDAV + + + Mot de passe WebDAV + + + Vérification de disponibilité WebDAV + + + Nom du dossier distant (optionnel) + + + Fichier de sauvegarde invalide + + + Filtre d’hôte + + + Actif + + + Source des fichiers Geo (optionnel) + + + Source des fichiers de jeux de règles sing-box (optionnel) + + + L’application d’outil de mise à niveau est introuvable + + + Source des jeux de règles de routage (optionnel) + + + Réglages régionaux prédéfinis + + + Région par défaut + + + Russie + + + Iran + + + Les utilisateurs de Chine peuvent ignorer + + + Scanner le QR code dans l’image + + + Adresse (URL) invalide + + + N’utilisez pas d’adresse d’abonnement HTTP non sécurisée + + + Installer la police sur le système, choisir ou saisir son nom, effet après redémarrage + + + Voulez-vous vraiment quitter ? + + + Mémo + + + Mot de passe sudo système + + + Le mot de passe sera vérifié en ligne de commande. En cas d’échec ou de dysfonctionnement, redémarrez l’application. Il n’est pas stocké et doit être saisi à chaque redémarrage. + + + *Mode XHTTP + + + JSON brut XHTTP Extra, format : { XHTTPObject } + + + Masquer dans la barre d’état à la fermeture de la fenêtre + + + Niveau de concurrence lors des tests multithread + + + Exceptions : ne pas utiliser le proxy pour ces adresses, séparées par des virgules (,). + + + Type de détection de trafic + + + Activer un second port d’écoute local + + + Socks : port local ; Socks2 : deuxième port local ; Socks3 : port LAN + + + Thème + + + Copier la cmd proxy terminal dans le presse-papiers + + + Recommencer le test des éléments échoués, {0} restants. Appuyez sur Échap pour arrêter... + + + Selon le résultat des tests + + + Supprimer les éléments invalides selon résultats tests + + + {0} résultats de test invalides supprimés. + + + Plage de ports sautés + + + Écrase le port ; pour plusieurs groupes, séparer par virgules (,) + + + Générer un groupe de stratégie depuis plusieurs profils + + + Xray aléatoire (multi-sélection) + + + Xray équilibrage (tourniquet) multi-sélection + + + Xray latence minimale (multi-sélection) + + + Xray le plus stable (multi-sélection) + + + sing-box latence minimale (multi-sélection) + + + Exporter + + + Adresse de test d’informations de connexion actuelles + + + Vous pouvez saisir un alias de configuration ; assurez-vous qu’il existe et qu’il est unique + + + Mot de passe incorrect, veuillez réessayer. + + + Mldsa65Verify + + + Ajouter [Anytls] + + + DNS distant + + + DNS direct + + + Via le proxy ; assurez-vous que le serveur distant est disponible + + + Stratégie de résolution xray freedom + + + Stratégie de résolution directe sing-box + + + Stratégie de résolution distante sing-box + + + Ajouter des hôtes DNS courants + + + FakeIP + + + Bloquer les requêtes SVCB et HTTPS + + + Hôtes DNS : (« domaine1 ip1 ip2 » une ligne par entrée) + + + Paramètres DNS de base + + + Paramètres DNS avancés + + + Valider les IP des domaines de la région concernée + + + Après config, les IP renvoyées des domaines régionaux (ex. geosite:cn) seront vérifiées ; seules les IP attendues seront retournées. + + + Activer le DNS personnalisé + + + DNS personnalisé activé ; la configuration de cette page sera ignorée + + + Une fois activé, bloque les requêtes ECH et de disponibilité HTTP/3 + + + Veuillez saisir un modèle de configuration valide + + + Paramètres du modèle de config complet + + + Activer le modèle de config. complet + + + Modèle de configuration complet v2ray + + + Ajoute seulement la config sortante, routing.balancers et routing.rules.outboundTag. Voir la doc. + + + N’ajoutez pas de sorties pour protocoles non-proxy. + + + Définir le tag de proxy amont + + + Modèle de configuration complet sing-box + + + Ajoute uniquement la configuration des sortants et des endpoints ; cliquer pour voir la documentation + + + Cette fonction s’adresse aux utilisateurs avancés et aux besoins spécifiques. Une fois activée, les paramètres de base du Core, du DNS et du routage sont ignorés. Vous devez vous assurer que la configuration des ports du proxy système, des statistiques de trafic, etc., est correcte — tout est à votre charge. + + + Début de l’analyse et du traitement du contenu d’abonnement + + + Choisir une config. + + + Actif globalement par défaut, avec filtre FakeIP intégré ; ne fonctionne que dans sing-box + + + Veuillez ajouter au moins une configuration + + + Groupe de stratégie + + + Chaîne de proxy + + + Latence minimale + + + Aléatoire + + + Round Robin + + + Le plus stable + + + Type de groupe de stratégie + + + Ajouter un groupe de stratégie + + + Ajouter une chaîne de proxy + + + Ajouter une sous-configuration + + + Supprimer une sous-configuration + + + Liste des configurations + + + Basculement (failover) + + + sing-box basculement (multi-sélection) + + + Xray basculement (multi-sélection) + + + Le cœur « {0} » ne prend pas en charge le type de réseau « {1} ». + + + Le cœur « {0} » ne prend pas en charge le protocole « {1} » avec le mode de transport « {2} ». + + + Le cœur « {0} » ne prend pas en charge le protocole « {1} ». + + + Chaîne de proxy : + + + Règle de routage sortante : + + + Groupe de stratégie : + + + L’alias « {0} » n’existe pas. + + + Le groupe « {0} » est vide. Veuillez ajouter au moins une configuration. + + + La propriété {0} est invalide, veuillez vérifier + + + Le groupe {0} ne peut pas se référencer lui-même ni créer de référence circulaire + + + Protocole « {0} » non pris en charge. + + + Si le système n’a pas de zone de notif., n’activez pas cette option + + + Type de règle + + + Des règles peuvent être définies séparément pour Routing et DNS ; ALL les applique aux deux + + + DNS d’amorçage + + + Résoudre le nom du serveur DNS ; doit être spécifié en IP + + + Test 1-clic de latence réelle + + + Ajout auto des configs filtrées depuis les groupes d’abonnement + + + Certificate Pinning + + + Certificat serveur (format PEM, facultatif) +Si le certificat est défini, il est fixé et l’option « Ignorer la vérification » est désactivée. + +Si un certificat auto-signé est utilisé ou si le système contient une CA non fiable ou malveillante, l’action « Obtenir le certificat » peut échouer. + + + Obtenir le certificat + + + Obtenir la chaîne de certificats + + + Veuillez définir un domaine valide + + + Certificat non configuré + + + Certificat configuré + + + Chemin fichier PAC personnalisé + + + Chemin script proxy système personnalisé + + + Afficher dans le Dock de macOS (redém. requis) + + diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index 1ce9d8e2..ecb37771 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -672,8 +672,8 @@ Core: alapbeállítások - - V2ray DNS beállítások + + V2ray Custom DNS Core: KCP beállítások @@ -744,9 +744,6 @@ 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 @@ -889,7 +886,7 @@ Késleltetés (ms) - Sebesség (M/s) + Sebesség (MB/s) Nem sikerült futtatni a Core-t, kérjük, ellenőrizze a prompt információt @@ -1008,8 +1005,8 @@ DNS beállítások - - sing-box DNS beállítások + + sing-box Custom DNS Kérjük, töltse ki a DNS struktúrát, kattintson a dokumentum megtekintéséhez @@ -1374,22 +1371,22 @@ A portot lefedi, vesszővel (,) elválasztva - - Több konfiguráció egyéni konfigurációra + + Generate Policy Group from Multiple Profiles - + 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 @@ -1416,19 +1413,10 @@ Domestic DNS - - Outbound DNS Resolution (sing-box) + + Via proxy — please ensure remote availability - - Resolve Outbound Domains - - - sing-box DoH Resolver Server - - - Fallback DNS Resolution, Suggest IP - - + xray Freedom Resolution Strategy @@ -1440,9 +1428,6 @@ Add Common DNS Hosts - - The sing-box DoH resolution server can be overwritten - FakeIP @@ -1471,7 +1456,7 @@ Custom DNS Enabled, This Page's Settings Invalid - Prevent domain-based routing rules from failing + Block ECH and HTTP/3 availability checks when enabled Please fill in the correct config template @@ -1512,4 +1497,139 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Configuration List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + {0} Group cannot reference itself or have a circular reference + + + Not support protocol '{0}'. + + + If the system does not have a tray function, please do not enable it + + + You can set separate rules for Routing and DNS, or select "ALL" to apply to both + + + Rule Type + + + Bootstrap DNS + + + Resolve DNS server domains, requires IP + + + Test real delay + + + Auto add filtered configuration from subscription groups + + + Certificate Pinning + + + Server Certificate (PEM format, optional) +When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. + +The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. + + + Fetch Certificate + + + Fetch Certificate Chain + + + Please set a valid domain + + + Certificate not set + + + Certificate set + + + Custom PAC file path + + + Custom system proxy script file path + + + macOS displays this in the Dock (requires restart) + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 74bee097..e0524438 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -672,8 +672,8 @@ Core: basic settings - - V2ray DNS settings + + V2ray Custom DNS Core: KCP settings @@ -744,9 +744,6 @@ System proxy settings - - Enable Security Protocol TLS v1.3 (subscription/update) - Tray right-click menu Configurations display limit @@ -889,7 +886,7 @@ Delay (ms) - Speed (M/s) + Speed (MB/s) Failed to run Core, please check the prompt information @@ -1008,8 +1005,8 @@ DNS Settings - - sing-box DNS settings + + sing-box Custom DNS Please fill in DNS Structure, Click to view the document @@ -1374,22 +1371,22 @@ Will cover the port, separate with commas (,) - - Multi-Configuration to custom configuration + + Generate Policy Group from Multiple Profiles - + 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 @@ -1416,19 +1413,10 @@ Domestic DNS - - Outbound DNS Resolution (sing-box) + + Via proxy — please ensure remote availability - - Resolve Outbound Domains - - - sing-box DoH Resolver Server - - - Fallback DNS Resolution, Suggest IP - - + xray Freedom Resolution Strategy @@ -1440,9 +1428,6 @@ Add Common DNS Hosts - - The sing-box DoH resolution server can be overwritten - FakeIP @@ -1471,7 +1456,7 @@ Custom DNS Enabled, This Page's Settings Invalid - Prevent domain-based routing rules from failing + Block ECH and HTTP/3 availability checks when enabled Please fill in the correct config template @@ -1512,4 +1497,139 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Configuration List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + {0} Group cannot reference itself or have a circular reference + + + Not support protocol '{0}'. + + + If the system does not have a tray function, please do not enable it + + + You can set separate rules for Routing and DNS, or select "ALL" to apply to both + + + Rule Type + + + Bootstrap DNS + + + Resolve DNS server domains, requires IP + + + Test real delay + + + Auto add filtered configuration from subscription groups + + + Certificate Pinning + + + Server Certificate (PEM format, optional) +When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. + +The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. + + + Fetch Certificate + + + Fetch Certificate Chain + + + Please set a valid domain + + + Certificate not set + + + Certificate set + + + Custom PAC file path + + + Custom system proxy script file path + + + macOS displays this in the Dock (requires restart) + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 1b218cf6..486eb709 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -672,8 +672,8 @@ Ядро: базовые настройки - - Настройки DNS V2ray + + V2ray Custom DNS Ядро: настройки KCP @@ -744,9 +744,6 @@ Настройки системного прокси - - Включить протокол безопасности TLS v1.3 (обновление подписки) - Лимит серверов в меню трея @@ -1008,8 +1005,8 @@ Настройки DNS - - Настройки DNS sing-box + + sing-box Custom DNS Заполните структуру DNS, нажмите, чтобы открыть документ @@ -1374,22 +1371,22 @@ Заменит указанный порт, перечисляйте через запятую (,) - - От мультиконфигурации к пользовательской конфигурации + + Generate Policy Group from Multiple Profiles - + Случайный (Xray) - + Круговой (Xray) - + Минимальное RTT (минимальное время туда-обратно) (Xray) - + Минимальная нагрузка (Xray) - + Минимальное RTT (минимальное время туда-обратно) (sing-box) @@ -1416,19 +1413,10 @@ Внутренний DNS - - Резолвер DNS для исходящих (sing-box) + + Via proxy — please ensure remote availability - - Разрешать домены для исходящих соединений - - - Сервер DoH-резолвера (sing-box) - - - Резервное DNS-разрешение (рекомендуется указывать IP) - - + Стратегия резолвинга Freedom (Xray) @@ -1440,9 +1428,6 @@ Добавить стандартные записи hosts (DNS) - - Сервер DoH-резолвера sing-box можно переопределить - FakeIP @@ -1471,7 +1456,7 @@ Включён пользовательский DNS — настройки на этой странице не применяются - Предотвращает сбои доменных правил маршрутизации + Block ECH and HTTP/3 availability checks when enabled Пожалуйста, заполните корректный шаблон конфигурации @@ -1512,4 +1497,139 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Configuration List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + {0} Group cannot reference itself or have a circular reference + + + Not support protocol '{0}'. + + + If the system does not have a tray function, please do not enable it + + + You can set separate rules for Routing and DNS, or select "ALL" to apply to both + + + Rule Type + + + Bootstrap DNS + + + Resolve DNS server domains, requires IP + + + Test real delay + + + Auto add filtered configuration from subscription groups + + + Certificate Pinning + + + Server Certificate (PEM format, optional) +When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. + +The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. + + + Fetch Certificate + + + Fetch Certificate Chain + + + Please set a valid domain + + + Certificate not set + + + Certificate set + + + Custom PAC file path + + + Custom system proxy script file path + + + macOS displays this in the Dock (requires restart) + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index e85ed53c..27e3a856 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -121,7 +121,7 @@ 导出分享链接至剪贴板成功 - 请先检查配置文件设置 + 请先检查设置 配置格式不正确 @@ -133,7 +133,7 @@ 下载开始... - 转换配置文件失败 + 转换配置失败 生成默认配置文件失败 @@ -142,10 +142,10 @@ 获取默认配置失败 - 导入自定义配置文件失败 + 导入自定义配置失败 - 读取配置文件失败 + 读取配置失败 请填写正确格式的端口 @@ -265,13 +265,13 @@ 请选择协议 - 请先选择配置文件 + 请先选择配置 - 配置文件去重完成。原数量: {0},现数量: {1}。 + 配置去重完成。原数量: {0},现数量: {1}。 - 是否确定移除配置文件? + 是否确定移除? 客户端配置文件保存在:{0} @@ -283,10 +283,10 @@ 配置成功。 {0} - 成功导入自定义配置文件 + 成功导入自定义配置 - 成功从剪贴板导入 {0} 个配置文件 + 成功从剪贴板导入 {0} 个配置 扫描导入分享链接成功 @@ -385,7 +385,7 @@ 所有 - 请浏览导入配置文件配置 + 请浏览导入配置 测试中... @@ -397,7 +397,7 @@ 本地 - 配置文件过滤器,按回车执行 + 过滤器,按回车执行 检查更新 @@ -424,7 +424,7 @@ 路由设置 - 配置文件 + 配置项 设置 @@ -475,55 +475,55 @@ 扫描屏幕上的二维码 (Ctrl+S) - 克隆所选配置文件 + 克隆所选 - 移除重复的配置文件 + 移除重复 - 移除所选配置文件 (多选) (Delete) + 移除所选 (多选) (Delete) - 设为活动配置文件 (Enter) + 设为活动 (Enter) 清除所有服务统计数据 - 测试配置文件真连接延迟 (多选) (Ctrl+R) + 测试真连接延迟 (多选) (Ctrl+R) 按测试结果排序 - 测试配置文件速度 (多选) (Ctrl+T) + 测试速度 (多选) (Ctrl+T) - 测试配置文件延迟 Tcping (多选) (Ctrl+O) + 测试延迟 Tcping (多选) (Ctrl+O) - 导出所选配置文件完整配置 + 导出所选完整配置 导出分享链接至剪贴板 (多选) (Ctrl+C) - 添加自定义配置文件 + 添加自定义配置 - 添加 [Shadowsocks] 配置文件 + 添加 [Shadowsocks] - 添加 [SOCKS] 配置文件 + 添加 [SOCKS] - 添加 [Trojan] 配置文件 + 添加 [Trojan] - 添加 [VLESS] 配置文件 + 添加 [VLESS] - 添加 [VMess] 配置文件 + 添加 [VMess] 全选 (Ctrl+A) @@ -672,8 +672,8 @@ Core: 基础设置 - - v2ray DNS 设置 + + v2ray 自定义 DNS Core: KCP 设置 @@ -688,7 +688,7 @@ Outbound Freedom domainStrategy - 自动调整配置文件列宽在更新订阅后 + 自动调整配置列宽在更新订阅后 检查 Pre-Release 更新 (请谨慎启用) @@ -697,7 +697,7 @@ 例外 - 例外:对于下列字符开头的地址,不使用代理配置文件。使用分号 (;) 分隔。 + 例外:对于下列字符开头的地址,不使用代理配置。使用分号 (;) 分隔。 显示实时速度 (需重启) @@ -744,11 +744,8 @@ 系统代理设置 - - 启用安全协议 TLS v1.3 (订阅/检查更新) - - 托盘右键菜单配置文件展示数量限制 + 托盘右键菜单配置展示数量限制 开启 UDP @@ -781,7 +778,7 @@ Pac 模式 - 分享配置文件 (Ctrl+F) + 分享 (Ctrl+F) 路由 @@ -889,7 +886,7 @@ 延迟 (ms) - 速度 (M/s) + 速度 (MB/s) 运行 Core 失败,请查看提示信息 @@ -913,7 +910,7 @@ 移至订阅分组 - 启用配置文件拖放排序 (需重启) + 启用配置拖放排序 (需重启) 自动刷新 @@ -922,10 +919,10 @@ 跳过测试 - 编辑配置文件 (Ctrl+D) + 编辑 (Ctrl+D) - 主界面双击设为活动配置文件 + 主界面双击设为活动 测试完成 @@ -1005,8 +1002,8 @@ DNS 设置 - - sing-box DNS 设置 + + sing-box 自定义 DNS 请填写 DNS JSON 结构,点击查看文档 @@ -1030,7 +1027,7 @@ Domain - 添加 [Hysteria2] 配置文件 + 添加 [Hysteria2] Hysteria 最大带宽 (Up/Dw) @@ -1039,19 +1036,19 @@ 使用系统 hosts - 添加 [TUIC] 配置文件 + 添加 [TUIC] 拥塞控制算法 - 前置代理配置文件别名 + 前置代理配置别名 - 落地代理配置文件別名 + 落地代理配置別名 - 请确保配置文件别名存在并唯一 + 请确保配置别名存在并唯一 自动路由 @@ -1072,7 +1069,7 @@ 启用 IPv6 - 添加 [WireGuard] 配置文件 + 添加 [WireGuard] PrivateKey @@ -1105,7 +1102,7 @@ *grpc Authority - 添加 [HTTP] 配置文件 + 添加 [HTTP] 和分组前置代理冲突 @@ -1195,13 +1192,13 @@ 延迟测试 - 当前部分节点延迟测试 + 当前部分延迟测试 刷新 - 设为活动节点 (Enter) + 设为活动 (Enter) Outbound 默认解析策略 @@ -1219,7 +1216,7 @@ 导出分享链接至剪贴板 (多选) Base64 编码 - 导出所选配置文件完整配置至剪贴板 + 导出所选完整配置至剪贴板 显示或隐藏主界面 @@ -1336,7 +1333,7 @@ 多线程测试时的并发数量 - 例外:对于下列地址不使用代理配置文件。使用逗号 (,) 分隔。 + 例外:对于下列地址不使用代理配置。使用逗号 (,) 分隔。 流量探测类型 @@ -1371,32 +1368,32 @@ 会覆盖端口,多组时用逗号 (,) 隔开 - - 多配置文件产生自定义配置 (多选) + + 多选生成策略组 - - 多配置文件随机 Xray + + 多选随机 Xray - - 多配置文件负载均衡 Xray + + 多选负载均衡 Xray - - 多配置文件最低延迟 Xray + + 多选最低延迟 Xray - - 多配置文件最稳定 Xray + + 多选最稳定 Xray - - 多配置文件最低延迟 sing-box + + 多选最低延迟 sing-box - 导出配置文件 + 导出 当前连接信息测试地址 - 可以填写配置文件别名,请确保存在并唯一 + 可以填写配置别名,请确保存在并唯一 密码错误,请重试。 @@ -1405,7 +1402,7 @@ Mldsa65Verify - 添加 [Anytls] 配置文件 + 添加 [Anytls] 远程 DNS @@ -1413,19 +1410,10 @@ 直连 DNS - - 出站 DNS 解析(sing-box) + + 通过代理,请确保远程可用 - - 解析出站域名 - - - sing-box DoH 解析服务器 - - - 兜底解析其他 DNS 域名,建议设为 ip - - + xray freedom 解析策略 @@ -1437,9 +1425,6 @@ 添加常用 DNS Hosts - - 开启后可覆盖 sing-box DoH 解析服务器 - FakeIP @@ -1468,7 +1453,7 @@ 自定义 DNS 已启用,此页面配置将无效 - 避免域名分流规则失效 + 开启后将阻止 ECH 和 HTTP/3 可用性查询 请填写正确的配置模板 @@ -1504,9 +1489,144 @@ 开始解析和处理订阅内容 - 选择配置文件 + 选择配置 默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效 + + 请至少添加一个配置 + + + 策略组 + + + 链式代理 + + + 最低延迟 + + + 随机 + + + 负载均衡 + + + 最稳定 + + + 策略组类型 + + + 添加策略组 + + + 添加链式代理 + + + 添加子配置 + + + 删除子配置 + + + 子配置项 + + + 故障转移 + + + 多选故障转移 sing-box + + + 多选故障转移 Xray + + + 核心 '{0}' 不支持网络类型 '{1}'。 + + + 核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}'。 + + + 核心 '{0}' 不支持协议 '{1}'。 + + + 代理链: + + + 路由规则出站: + + + 策略组: + + + 别名 '{0}' 不存在。 + + + 组“{0}”为空。请至少添加一个配置。 + + + {0}属性无效,请检查 + + + {0} 分组不能引用自身或循环引用 + + + 不支持协议 '{0}'。 + + + 如果系统没有托盘功能,请不要开启 + + + 规则类型 + + + 可对 Routing 和 DNS 单独设定规则,ALL 则都生效 + + + Bootstrap DNS + + + 解析 DNS 服务器域名,需指定为 IP + + + 一键测试真连接延迟 + + + 自动从订阅分组添加过滤后的配置 + + + 固定证书 + + + 服务器证书(PEM 格式,可选) +当指定此证书后,将固定该证书,并禁用“跳过证书验证”选项。 + +“获取证书”操作可能失败,原因可能是使用了自签证书,或系统中存在不受信任或恶意的 CA。 + + + 获取证书 + + + 获取证书链 + + + 请设置有效的域名 + + + 证书未设置 + + + 证书已设置 + + + 自定义 PAC 文件路径 + + + 自定义系统代理脚本文件路径 + + + macOS 在 Dock 栏中显示 (需重启) + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 4b458924..d77f01cf 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -121,7 +121,7 @@ 匯出分享連結至剪貼簿成功 - 請先檢查設定檔設定 + 請先檢查設定 設定格式不正確 @@ -133,7 +133,7 @@ 下載開始... - 轉換設定檔失敗 + 轉換設定失敗 生成預設設定檔失敗 @@ -142,10 +142,10 @@ 獲取預設設定失敗 - 匯入自訂設定設定檔失敗 + 匯入自訂設定失敗 - 讀取設定檔失敗 + 讀取設定失敗 請填寫正確格式的埠 @@ -265,13 +265,13 @@ 請選擇協定 - 請先選擇設定檔 + 請先選擇設定 - 設定檔去重完成。原數量: {0},現數量: {1}。 + 去重完成。原數量: {0},現數量: {1}。 - 是否確定移除設定檔? + 是否確定移除? 用戶端設定檔儲存在:{0} @@ -283,10 +283,10 @@ 設定成功。{0} - 成功匯入自訂設定設定檔 + 成功匯入自訂節點 - 成功從剪貼簿匯入 {0} 個設定檔 + 成功從剪貼簿匯入 {0} 個節點 掃描匯入分享連結成功 @@ -385,7 +385,7 @@ 所有 - 請瀏覽匯入設定檔設定 + 請瀏覽匯入設定 測試中... @@ -397,7 +397,7 @@ 本機 - 設定檔過濾,按 Enter 執行 + 過濾器,按 Enter 執行 檢查更新 @@ -475,55 +475,55 @@ 掃描螢幕上的二維碼 (Ctrl+S) - 複製所選設定檔 + 複製所選 - 移除重複的設定檔 + 移除重複 - 移除所選設定檔 (多選) (Delete) + 移除所選 (多選) (Delete) - 設為活動設定檔 (Enter) + 設為活動 (Enter) 清除所有服務統計資料 - 測試設定檔真連線延遲 (多選) (Ctrl+R) + 測試真連線延遲 (多選) (Ctrl+R) 按測試結果排序 - 測試設定檔速度 (多選) (Ctrl+T) + 測試速度 (多選) (Ctrl+T) - 測試設定檔延遲 Tcping (多選) (Ctrl+O) + 測試延遲 Tcping (多選) (Ctrl+O) - 匯出所選設定檔完整設定 + 匯出所選完整設定 匯出分享連結至剪貼簿 (多選) (Ctrl+C) - 新增自訂設定設定檔 + 新增自訂節點 - 新增 [Shadowsocks] 設定檔 + 新增 [Shadowsocks] 節點 - 新增 [SOCKS] 設定檔 + 新增 [SOCKS] 節點 - 新增 [Trojan] 設定檔 + 新增 [Trojan] 節點 - 新增 [VLESS] 設定檔 + 新增 [VLESS] 節點 - 新增 [VMess] 設定檔 + 新增 [VMess] 節點 全選 (Ctrl+A) @@ -672,8 +672,8 @@ Core: 基礎設定 - - V2ray DNS 設定 + + v2ray 自訂 DNS Core: KCP 設定 @@ -688,7 +688,7 @@ Outbound Freedom domainStrategy - 在更新訂閱後自動調整設定檔列寬 + 在更新訂閱後自動調整列寬 檢查 Pre-Release 更新 (請謹慎啟用) @@ -697,7 +697,7 @@ 例外 - 例外:對於下列字元開頭的位址,不使用代理設定檔。使用分號 (;) 分隔。 + 例外:對於下列字元開頭的位址,不使用代理。使用分號 (;) 分隔。 顯示即時速度(需重啟) @@ -744,11 +744,8 @@ 系統代理設定 - - 啟用安全協定 TLS v1.3 (訂閱/檢查更新) - - 工具列右鍵選單設定檔展示數量限制 + 工具列右鍵選單設定展示數量限制 開啟 UDP @@ -781,7 +778,7 @@ PAC 模式 - 分享設定檔 (Ctrl+F) + 分享 (Ctrl+F) 路由 @@ -883,13 +880,13 @@ 請勿將代理伺服器用於本機(Intranet)位址 - 一鍵多執行緒測試延遲和速度 (Ctrl+E) + 一鍵延遲與速度測試 (Ctrl+E) 延遲 (ms) - 速度 (M/s) + 速度 (MB/s) 執行 Core 失敗,請查看提示訊息 @@ -913,7 +910,7 @@ 移至訂閱分組 - 啟動設定檔拖放排序 (需重啟) + 啟用拖放排序 (需重啟) 自動重新整理 @@ -922,10 +919,10 @@ 跳過測試 - 編輯設定檔 (Ctrl+D) + 編輯 (Ctrl+D) - 主介面輕按兩下設為活動設定檔 + 主介面輕按兩下設為活動 測試完成 @@ -943,7 +940,7 @@ 目前字型 (需重啟) - 複製字型 TTF/TTC 檔案到目錄 guiFonts,重啟設定 + 複製字型 TTF/TTC 檔案到目錄 guiFonts,重新啟動後生效 Pac 連接埠 = +3;Xray API 連接埠 = +4;mihomo API 連接埠 = +5; @@ -1003,10 +1000,10 @@ 不需要轉換時請留空 - DNS 設定 + DNS設定 - - sing-box DNS 設定 + + sing-box 自訂 DNS 請填寫 DNS JSON 結構,點擊查看檔案 @@ -1030,7 +1027,7 @@ Domain - 添加 [Hysteria2] 設定檔 + 新增 [Hysteria2] 節點 Hysteria 最大頻寬 (Up/Dw) @@ -1039,19 +1036,19 @@ 使用系統 hosts - 新增 [TUIC] 設定檔 + 新增 [TUIC] 節點 擁塞控制算法 - 前置代理設定檔別名 + 前置代理節點別名 - 落地代理設定檔別名 + 落地代理節點別名 - 請確保設定檔別名存在並且唯一 + 請確保節點別名存在並且唯一 自動路由 @@ -1072,7 +1069,7 @@ 啟用 IPv6 - 添加 [WireGuard] 設定檔 + 新增 [WireGuard] 節點 PrivateKey @@ -1105,7 +1102,7 @@ *grpc Authority - 新增 [HTTP] 設定檔 + 新增 [HTTP] 節點 和分組前置代理衝突 @@ -1219,13 +1216,13 @@ 匯出分享連結至剪貼簿 (多選) Base64 編碼 - 匯出所選設定檔完整設定至剪貼簿 + 匯出所選完整設定至剪貼簿 顯示或隱藏主介面 - 自訂設定的 Socks 連接埠 + 自訂 Socks 連接埠 備份和還原 @@ -1309,7 +1306,7 @@ 請不要使用不安全的 HTTP 協定訂閱位址 - 安裝字體到系統中,選擇或填入字體名稱,重新啟動設定 + 安裝字體到系統中,選擇或填入字體名稱,重新啟動後生效 是否確定退出? @@ -1336,7 +1333,7 @@ 多執行緒測試時的並發數量 - 例外:對於下列位址不使用代理設定檔,使用逗號 (,) 分隔。 + 例外:對於下列位址不使用代理,使用逗號 (,) 分隔。 流量探測類型 @@ -1371,32 +1368,32 @@ 會覆蓋埠,多組時用逗號 (,) 隔開 - - 多設定檔產生自訂配置 (多選) + + 多選生成策略組 - - 多設定檔隨機 Xray + + 多選隨機 Xray - - 多設定檔負載平衡 Xray + + 多選負載平衡 Xray - - 多設定檔最低延遲 Xray + + 多選最低延遲 Xray - - 多設定檔最穩定 Xray + + 多選最穩定 Xray - - 多設定檔最低延遲 sing-box + + 多選最低延遲 sing-box - 匯出設定檔 + 匯出 目前連接資訊測試地址 - 可以填寫設定檔別名,請確保存在並唯一 + 可以填寫節點別名,請確保存在並唯一 密碼錯誤,請重試。 @@ -1405,108 +1402,231 @@ Mldsa65Verify - 新增 [Anytls] 設定檔 + 新增 [Anytls] 節點 - Remote DNS + 遠程 DNS - Domestic DNS + 直連 DNS - - Outbound DNS Resolution (sing-box) + + 通过代理,请确保远程可用 - - Resolve Outbound Domains - - - sing-box DoH Resolver Server - - - Fallback DNS Resolution, Suggest IP - - - xray Freedom Resolution Strategy + + xray freedom 解析策略 - sing-box Direct Resolution Strategy + sing-box 直連解析策略 - sing-box Remote Resolution Strategy + sing-box 遠程解析策略 - Add Common DNS Hosts - - - The sing-box DoH resolution server can be overwritten + 新增常用 DNS Hosts FakeIP - Block SVCB and HTTPS Queries + 阻止 SVCB 和 HTTPS 查詢 - DNS Hosts: ("domain1 ip1 ip2" per line) + DNS Hosts:(“網域名稱1 ip1 ip2” 一行一個) - Basic DNS Settings + DNS 基礎設定 - Advanced DNS Settings + DNS 進階設定 - Validate Regional Domain IPs + 校驗相應地區域名 IP - When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs + 配置後,會對相應地區域名(如 geosite:cn)的返回 IP 進行校驗,僅返回期望 IP - Enable Custom DNS + 啟用自訂 DNS - Custom DNS Enabled, This Page's Settings Invalid + 自訂 DNS 已啟用,此頁面配置將無效 - Prevent domain-based routing rules from failing + 開啟後將阻止 ECH 和 HTTP/3 可用性查詢 - Please fill in the correct config template + 請填寫正確的配置範本 - Full Config Template Setting + 完整配置範本設定 - Enable Full Config Template + 啟用完整配置範本 - v2ray Full Config Template + v2ray 完整配置範本 - Add Outbound Config Only, routing.balancers and routing.rules.outboundTag, Click to view the document + 僅添加出站配置,routing.balancers 和 routing.rules.outboundTag,點擊查看文檔 - Do Not Add Non-Proxy Protocol Outbound + 不添加非代理協定出站 - Set Upstream Proxy Tag + 設定上游代理 tag - sing-box Full Config Template + sing-box 完整配置範本 - 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. + 此功能供高級用戶和有特殊需求的用戶使用。 啟用此功能後,將忽略 Core 的基礎設定,DNS 設定 ,路由設定。你需要保證系統代理的埠和流量統計等功能的配置正確,一切都由你來設定。 開始解析和處理訂閱內容 - Select Profile + 選擇節點 - Applies globally by default, with built-in FakeIP filtering (sing-box only). + 默認全域生效,內置 FakeIP 過濾,僅在 sing-box 中生效 + + + 請至少添加一個節點 + + + 策略組 + + + 鏈式代理 + + + 最低延遲 + + + 隨機 + + + 負載均衡 + + + 最穩定 + + + 策略組類型 + + + 添加策略組 + + + 添加鏈式代理 + + + 添加子配置 + + + 刪除子配置 + + + 子配置項 + + + 容錯移轉 + + + 多選容錯移轉 sing-box + + + 多選容錯移轉 Xray + + + 核心 '{0}' 不支援網路類型 '{1}'. + + + 核心 '{0}' 在使用傳輸方式 '{2}' 時不支援協定 '{1}'. + + + 核心 '{0}' 不支援協定 '{1}'. + + + 代理鏈: + + + 路由規則出站: + + + 策略組: + + + 別名 '{0}' 不存在。 + + + 組“{0}”為空.請至少添加一個配置。 + + + {0}屬性無效,請檢查 + + + {0} 分組不能引用自身或循環引用 + + + 不支援協定 '{0}'. + + + 如果系統沒有託盤功能,請不要開啟 + + + 規則類型 + + + 可對 Routing 和 DNS 單獨設定規則,ALL 則都生效 + + + Bootstrap DNS + + + 解析 DNS 伺服器網域名稱,需指定為 IP + + + 一鍵測試真連線延遲 + + + 自動從訂閱分組新增過濾後的配置 + + + 憑證綁定 + + + 伺服器憑證(PEM 格式,可選) +若已指定,憑證將會被綁定,並且「跳過憑證驗證」將被停用。 + +若使用自簽憑證,或系統中存在不受信任或惡意的 CA,「取得憑證」動作可能會失敗。 + + + 獲取憑證 + + + 獲取憑證鏈 + + + 請設定有效的網域名稱 + + + 尚未設定憑證 + + + 已設定憑證 + + + 自訂 PAC 檔案路徑 + + + 自訂系統代理程式腳本檔案路徑 + + + macOS 在 Dock 欄顯示 (需重啟) \ 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 index 7f62a532..21e17408 100644 --- a/v2rayN/ServiceLib/Sample/kill_as_sudo_linux_sh +++ b/v2rayN/ServiceLib/Sample/kill_as_sudo_linux_sh @@ -28,15 +28,15 @@ fi 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 @@ -47,6 +47,18 @@ echo "============================================" echo "Starting termination of process $PID and all its children" echo "============================================" +# Try graceful termination first +echo "Attempting graceful termination (SIGTERM) of PID: $PID" +kill -15 "$PID" 2>/dev/null || true +sleep 1 +# If still running, fall back to kill_children +if ps -p $PID > /dev/null; then + echo "Process $PID did not exit after SIGTERM; proceeding with forced termination of its children and itself" +else + echo "Process $PID exited cleanly after SIGTERM" + exit 0 +fi + # Find and kill all child processes kill_children "$PID" diff --git a/v2rayN/ServiceLib/Sample/kill_as_sudo_osx_sh b/v2rayN/ServiceLib/Sample/kill_as_sudo_osx_sh index 94011d6f..61160fa2 100644 --- a/v2rayN/ServiceLib/Sample/kill_as_sudo_osx_sh +++ b/v2rayN/ServiceLib/Sample/kill_as_sudo_osx_sh @@ -42,6 +42,20 @@ echo "============================================" echo "Starting termination of process $PID and all its descendants" echo "============================================" +# Try graceful termination first +echo "Attempting graceful termination (SIGTERM) of PID: $PID" +kill -15 "$PID" 2>/dev/null || true +sleep 1 + +# If still running, fall back to kill_descendants +# Use the macOS-native 'kill -0' check +if kill -0 $PID 2>/dev/null; then + echo "Process $PID did not exit after SIGTERM; proceeding with forced termination of its descendants and itself" +else + echo "Process $PID exited cleanly after SIGTERM" + exit 0 +fi + # Find and kill all descendant processes kill_descendants "$PID" diff --git a/v2rayN/ServiceLib/Sample/pac b/v2rayN/ServiceLib/Sample/pac index 1378c49f..f4f5939b 100644 --- a/v2rayN/ServiceLib/Sample/pac +++ b/v2rayN/ServiceLib/Sample/pac @@ -7,12 +7,12 @@ var rules = [ "030buy.com", "0rz.tw", "1-apple.com.tw", - "10.tt", "1000giri.net", "100ke.org", "10beasts.net", "10conditionsoflove.com", "10musume.com", + "111666.best", "123rf.com", "12bet.com", "12vpn.com", @@ -29,7 +29,6 @@ var rules = [ "177pic.info", "17t17p.com", "18board.com", - "18board.info", "18comic.org", "18comic.vip", "18hmanga.click", @@ -42,20 +41,18 @@ var rules = [ "1984bbs.com", "1984bbs.org", "1991way.com", - "1998cdp.org", "1bao.org", "1dumb.com", "1e100.net", "1eew.com", "1lib.domains", + "1lib.sk", "1mobile.com", - "1mobile.tw", "1point3acres.com", "1pondo.tv", "2-hand.info", "2000fun.com", "2008xianzhang.info", - "2017.hk", "2021hkcharter.com", "2047.name", "2047.one", @@ -68,7 +65,6 @@ var rules = [ "233abc.com", "233v2.com", "24hrs.ca", - "24smile.org", "25u.com", "2du5.com", "2lipstube.com", @@ -87,11 +83,11 @@ var rules = [ "3ren.ca", "3tui.net", "404museum.com", - "43110.cf", "466453.com", "4bluestones.biz", "4chan.com", "4dq.com", + "4everland.io", "4everproxy.com", "4gtv.tv", "4irc.com", @@ -109,7 +105,7 @@ var rules = [ "5278.cc", "5299.tv", "55comic.com", - "5aimiku.com", + "5657.com.tw", "5i01.com", "5isotoi5.org", "5maodang.com", @@ -124,6 +120,7 @@ var rules = [ "666kb.com", "666pool.cn", "69shu.com", + "69shuba.com", "69shuba.cx", "6do.news", "6do.world", @@ -146,8 +143,7 @@ var rules = [ "8news.com.tw", "8world.com", "8z1.net", - "9001700.com", - "908taiwan.org", + "91dasai.com", "91jinman.com", "91porn.com", "91porny.com", @@ -169,16 +165,13 @@ var rules = [ "abc.net.au", "abc.xyz", "abchinese.com", - "abclite.net", "abebooks.co.uk", "abebooks.com", "ablwang.com", "aboluowang.com", - "about.google", "about.me", - "aboutgfw.com", + "abplive.com", "abs.edu", - "ac.jp", "acast.com", "accim.org", "accountkit.com", @@ -226,7 +219,6 @@ var rules = [ "agro.hk", "ai-kan.net", "ai-wen.net", - "ai.google", "aiosearch.com", "aiph.net", "airasia.com", @@ -308,9 +300,9 @@ var rules = [ "amnesty.tw", "amnestyusa.org", "amnyemachen.org", - "amoiist.com", "ampproject.org", "amtb-taipei.org", + "amuletmc.com", "anchor.fm", "anchorfree.com", "ancsconf.org", @@ -363,8 +355,8 @@ var rules = [ "aol.co.uk", "aol.com", "aolnews.com", + "aomedia.org", "aomiwang.com", - "ap.org", "apartmentratings.com", "apartments.com", "apat1989.org", @@ -384,6 +376,7 @@ var rules = [ "aplusvpn.com", "appadvice.com", "appbrain.com", + "appdefensealliance.dev", "appdownloader.net", "appledaily.com", "appledaily.com.hk", @@ -412,12 +405,14 @@ var rules = [ "arethusa.su", "arlingtoncemetery.mil", "army.mil", + "arstechnica.net", "art4tibet1998.org", "arte.tv", "artofpeacefoundation.org", "artstation.com", "artsy.net", "arunachalforests.gov.in", + "arvanstorage.ir", "asacp.org", "asdfg.jp", "asg.to", @@ -425,10 +420,7 @@ var rules = [ "asiaharvest.org", "asianage.com", "asianews.it", - "asianfreeforum.com", "asiansexdiary.com", - "asianspiss.com", - "asianwomensfilm.de", "asiaone.com", "asiatgp.com", "asiatimes.com", @@ -447,13 +439,11 @@ var rules = [ "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", @@ -480,7 +470,6 @@ var rules = [ "azerbaycan.tv", "azerimix.com", "azirevpn.com", - "azubu.tv", "azurewebsites.net", "b-cdn.net", "b-ok.cc", @@ -525,7 +514,6 @@ var rules = [ "bartvpn.com", "bastillepost.com", "bayvoice.net", - "baywords.com", "bb-chat.tv", "bbc.co.uk", "bbc.com", @@ -583,6 +571,7 @@ var rules = [ "bet365.com", "betaclouds.net", "betfair.com", + "betterhash.net", "betternet.co", "bettervpn.com", "bettween.com", @@ -616,6 +605,7 @@ var rules = [ "biliworld.com", "billypan.com", "binance.com", + "binance.org", "binance.us", "binancezh.cc", "bing.com", @@ -633,7 +623,6 @@ var rules = [ "bitfinex.com", "bitget.com", "bithumb.com", - "bitinka.com.ar", "bitmex.com", "bitshare.com", "bitsnoop.com", @@ -648,6 +637,7 @@ var rules = [ "bl-doujinsouko.com", "blacked.com", "blacklogic.com", + "blackmagicdesign.com", "blackvpn.com", "blewpass.com", "blingblingsquad.net", @@ -660,7 +650,6 @@ var rules = [ "blockless.com", "blocktempo.com", "blog.de", - "blog.google", "blog.jp", "blogblog.com", "blogcatalog.com", @@ -756,17 +745,18 @@ var rules = [ "bodog88.com", "bolehvpn.net", "bonbonme.com", - "bonbonsex.com", "bonfoundation.org", "bongacams.com", "boobstagram.com", "book.com.tw", "bookdepository.com", "bookepub.com", + "booklive.jp", + "bookmeter.com", "books.com.tw", "booktopia.com.au", "bookwalker.com.tw", - "boomssr.com", + "bookwalker.jp", "bootstrapcdn.com", "borgenmagazine.com", "bot.nu", @@ -796,11 +786,9 @@ var rules = [ "breaking911.com", "breakingtweets.com", "breakwall.net", - "briefdream.com", "briian.com", "brill.com", "brizzly.com", - "brkmd.com", "broadbook.com", "broadpressinc.com", "brockbbs.com", @@ -811,6 +799,8 @@ var rules = [ "bsky.network", "bsky.social", "bt2mag.com", + "bt4g.org", + "bt4gprx.com", "bt95.com", "btaia.com", "btbit.net", @@ -826,6 +816,7 @@ var rules = [ "btguard.com", "btku.me", "btku.org", + "btloader.com", "btspread.com", "btsynckeys.com", "budaedu.org", @@ -868,6 +859,7 @@ var rules = [ "c-est-simple.com", "c-span.org", "c-spanvideo.org", + "c.gle", "c100tibet.org", "c2cx.com", "c3pool.com", @@ -881,15 +873,12 @@ var rules = [ "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", @@ -1021,7 +1010,6 @@ var rules = [ "chicagoncmtv.com", "china-mmm.jp.net", "china-mmm.net", - "china-mmm.sa.com", "china-review.com.ua", "china-week.com", "china101.com", @@ -1104,7 +1092,6 @@ var rules = [ "chrlawyers.hk", "chrome.com", "chromecast.com", - "chromeenterprise.google", "chromeexperiments.com", "chromercise.com", "chromestatus.com", @@ -1118,7 +1105,9 @@ var rules = [ "chushigangdrug.ch", "ci-en.jp", "cia.gov", + "cici.com", "ciciai.com", + "ciciaicdn.com", "cienen.com", "cineastentreff.de", "cipfg.org", @@ -1139,7 +1128,6 @@ var rules = [ "civilhrfront.org", "civiliangunner.com", "civilmedia.tw", - "civisec.org", "civitai.com", "cixiaoya.club", "cjb.net", @@ -1162,8 +1150,8 @@ var rules = [ "clips4sale.com", "cloakpoint.com", "cloudcone.com", + "cloudflare-dns.com", "cloudflare-ipfs.com", - "cloudfront.net", "cloudfunctions.net", "cloudokyo.cloud", "club1069.com", @@ -1192,17 +1180,15 @@ var rules = [ "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", + "coffeemanga.to", "coinbase.com", "coinbene.com", "coinegg.com", @@ -1217,12 +1203,10 @@ var rules = [ "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", @@ -1251,10 +1235,13 @@ var rules = [ "cotweet.com", "counter.social", "coursehero.com", + "covenantswatch.org.tw", "coze.com", "cpj.org", + "cpu-monkey.com", "cq99.us", "crackle.com", + "crashlytics.com", "crazypool.org", "crazys.cc", "crazyshit.com", @@ -1264,7 +1251,6 @@ var rules = [ "creaders.net", "creadersnet.com", "creativelab5.com", - "crisisresponse.google", "cristyli.com", "crocotube.com", "crossfire.co.kr", @@ -1287,6 +1273,7 @@ var rules = [ "ct.org.tw", "ctao.org", "ctfriend.net", + "ctinews.com", "ctitv.com.tw", "ctowc.org", "cts.com.tw", @@ -1312,7 +1299,6 @@ var rules = [ "cz.cc", "d-fukyu.com", "d.cash", - "d0z.net", "d100.net", "d2bay.com", "d2pass.com", @@ -1407,10 +1393,11 @@ var rules = [ "delicious.com", "democrats.org", "demosisto.hk", + "deno.com", + "deno.dev", "depositphotos.com", "derekhsu.homeip.net", "desc.se", - "design.google", "desipro.de", "dessci.com", "destroy-china.jp", @@ -1435,7 +1422,6 @@ var rules = [ "digitalnomadsproject.org", "diigo.com", "dilber.se", - "dingchin.com.tw", "dipity.com", "directcreative.com", "discoins.com", @@ -1474,7 +1460,6 @@ var rules = [ "dmm.com", "dns-dns.com", "dns-stuff.com", - "dns.google", "dns04.com", "dns05.com", "dns1.us", @@ -1496,10 +1481,8 @@ var rules = [ "dok-forum.net", "dolc.de", "dolf.org.hk", - "dollf.com", "domain.club.tw", "domain.glass", - "domains.google", "domaintoday.com.au", "donga.com", "dongtaiwang.com", @@ -1508,6 +1491,7 @@ var rules = [ "donmai.us", "dontfilter.us", "dontmovetochina.com", + "doom9.org", "doosho.com", "doourbest.org", "dorjeshugden.com", @@ -1518,10 +1502,8 @@ var rules = [ "doubibackup.com", "doubiyunbackup.com", "doublethinklab.org", - "doubmirror.cf", "douchi.space", "dougscripts.com", - "douhokanko.net", "doujincafe.com", "dowei.org", "dowjones.com", @@ -1545,14 +1527,12 @@ var rules = [ "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", @@ -1602,6 +1582,7 @@ var rules = [ "e-traderland.net", "e-zone.com.hk", "e123.hk", + "e621.net", "earlytibet.com", "earthcam.com", "earthvpn.com", @@ -1621,7 +1602,6 @@ var rules = [ "ebookee.com", "ebtcbank.com", "ecfa.org.tw", - "echainhost.com", "echofon.com", "ecimg.tw", "eckosia.org", @@ -1635,6 +1615,7 @@ var rules = [ "edmontonservice.com", "edns.biz", "edoors.com", + "edrdg.org", "edubridge.com", "edupro.org", "edx-cdn.org", @@ -1678,8 +1659,6 @@ var rules = [ "enlighten.org.tw", "entermap.com", "entnt.com", - "environment.google", - "epa.gov.tw", "epac.to", "episcopalchurch.org", "epochhk.com", @@ -1748,7 +1727,6 @@ var rules = [ "evozi.com", "evschool.net", "exam.gov.tw", - "exblog.co.jp", "exblog.jp", "exchristian.hk", "excite.co.jp", @@ -1766,7 +1744,6 @@ var rules = [ "extmatrix.com", "extrabux.com", "extremetube.com", - "exx.com", "ey.gov.tw", "eyevio.jp", "eyny.com", @@ -1877,7 +1854,6 @@ var rules = [ "fc2cn.com", "fc2web.com", "fda.gov.tw", - "fdbox.com", "fdc64.de", "fdc64.jp", "fdc64.org", @@ -1913,6 +1889,7 @@ var rules = [ "filmingfortibet.org", "filthdump.com", "financetwitter.com", + "financialexpress.com", "finchvpn.com", "findbook.tw", "findmespot.com", @@ -1977,7 +1954,6 @@ var rules = [ "fountmedia.io", "fourthinternational.org", "foxbusiness.com", - "foxdie.us", "foxgay.com", "foxsub.com", "foxtang.com", @@ -2086,6 +2062,8 @@ var rules = [ "ftx.com", "fucd.com", "fuchsia.dev", + "fuckccp.com", + "fuckccp.xyz", "fuckcnnic.net", "fuckgfw.org", "fuckgfw233.org", @@ -2101,7 +2079,7 @@ var rules = [ "furbo.org", "furhhdl.org", "furinkan.com", - "furl.net", + "furrybar.com", "futurechinaforum.org", "futuremessage.org", "fux.com", @@ -2147,6 +2125,7 @@ var rules = [ "gardennetworks.com", "gardennetworks.org", "gartlive.com", + "garudalinux.org", "gate-project.com", "gate.io", "gatecoin.com", @@ -2191,7 +2170,6 @@ var rules = [ "getchu.com", "getcloak.com", "getfoxyproxy.org", - "getfreedur.com", "getgom.com", "geti2p.net", "getiton.com", @@ -2221,6 +2199,7 @@ var rules = [ "ghanely.me", "ghidra-sre.org", "ghostpath.com", + "ghproxy.com", "ghut.org", "giantessnight.com", "gifree.com", @@ -2239,6 +2218,8 @@ var rules = [ "githubassets.com", "githubcopilot.com", "githubusercontent.com", + "gitlab.com", + "gitlab.net", "gizlen.net", "gjczz.com", "glarity.app", @@ -2264,6 +2245,7 @@ var rules = [ "gmiddle.net", "gmll.org", "gmodules.com", + "gmp4.com", "gmx.net", "gnci.org.hk", "gnews.org", @@ -2345,6 +2327,7 @@ var rules = [ "google.cl", "google.cm", "google.cn", + "google.co", "google.co.ao", "google.co.bw", "google.co.ck", @@ -2531,7 +2514,6 @@ var rules = [ "googlehosted.com", "googleideas.com", "googleinsidesearch.com", - "googlelabs.com", "googlelocal.nl", "googlemail.com", "googlemaps.sv", @@ -2540,7 +2522,6 @@ var rules = [ "googleplay.com", "googleplus.com", "googlescholar.com", - "googlesile.com", "googlesource.com", "googlesyndication.com", "googleusercontent.com", @@ -2555,8 +2536,10 @@ var rules = [ "got-game.org", "gotdns.ch", "gotgeeks.com", + "gotquestions.org", "gotrusted.com", "gotw.ca", + "gov.ir", "gov.taipei", "gov.tw", "gr8domain.biz", @@ -2575,7 +2558,6 @@ var rules = [ "great-roc.org", "greatfire.org", "greatfirewall.biz", - "greatfirewallofchina.net", "greatfirewallofchina.org", "greatroc.org", "greatroc.tw", @@ -2591,7 +2573,6 @@ var rules = [ "grok.com", "grotty-monday.com", "ground.news", - "grow.google", "gs-discuss.com", "gsearch.media", "gstatic.com", @@ -2606,7 +2587,6 @@ var rules = [ "guancha.org", "guaneryu.com", "guangming.com.my", - "guangnianvpn.com", "guardster.com", "guishan.org", "gumroad.com", @@ -2693,7 +2673,6 @@ var rules = [ "helpeachpeople.com", "helplinfen.com", "helpster.de", - "helpuyghursnow.org", "helpzhuling.org", "henduohao.com", "hentai.to", @@ -2705,12 +2684,12 @@ var rules = [ "heritage.org", "heroku.com", "herokuapp.com", + "herominers.com", "heungkongdiscuss.com", "hexieshe.com", "hexieshe.xyz", "hexxeh.net", "heyuedi.com", - "heywire.com", "heyzo.com", "hgamefree.info", "hgseav.com", @@ -2769,7 +2748,6 @@ var rules = [ "hkcmi.edu", "hkcnews.com", "hkcoc.com", - "hkctu.org.hk", "hkdailynews.com.hk", "hkday.net", "hkdc.us", @@ -2783,13 +2761,10 @@ var rules = [ "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", @@ -2804,7 +2779,6 @@ var rules = [ "hkusu.net", "hkvwet.com", "hkwcc.org.hk", - "hkzone.org", "hmoegirl.com", "hmonghot.com", "hmv.co.jp", @@ -2839,6 +2813,7 @@ var rules = [ "hotair.com", "hotav.tv", "hotcoin.com", + "hotcool.tw", "hotels.cn", "hotfrog.com.tw", "hotgoo.com", @@ -2930,7 +2905,6 @@ var rules = [ "huyandex.com", "hwadzan.tw", "hwayue.org.tw", - "hwinfo.com", "hxwk.org", "hxwq.org", "hybrid-analysis.com", @@ -2952,7 +2926,6 @@ var rules = [ "iav19.com", "iavian.net", "ibiblio.org", - "ibit.am", "iblist.com", "iblogserv-f.net", "ibros.org", @@ -2965,7 +2938,6 @@ var rules = [ "icij.org", "icl-fi.org", "icoco.com", - "iconfactory.net", "iconpaper.org", "icu-project.org", "idaiwan.com", @@ -2973,14 +2945,13 @@ var rules = [ "identi.ca", "idiomconnection.com", "idlcoyote.com", + "idope.se", "idouga.com", "idreamx.com", - "idsam.com", "idv.tw", "ieasy5.com", "ied2k.net", "ienergy1.com", - "iepl.us", "ifanqiang.com", "ifcss.org", "ifjc.org", @@ -3055,7 +3026,6 @@ var rules = [ "illusionfactory.com", "ilove80.be", "ilovelongtoes.com", - "im.tv", "im88.tw", "imageab.com", "imagefap.com", @@ -3088,6 +3058,7 @@ var rules = [ "incloak.com", "incredibox.fr", "independent.co.uk", + "india.com", "indiablooms.com", "indianarrative.com", "indiandefensenews.in", @@ -3098,7 +3069,7 @@ var rules = [ "indsr.org.tw", "info-graf.fr", "informer.com", - "ingress.com", + "infura.io", "inherit.live", "initiativesforchina.org", "inkbunny.net", @@ -3122,7 +3093,6 @@ var rules = [ "inthenameofconfuciusmovie.com", "invidio.us", "inxian.com", - "iownyour.biz", "iownyour.org", "ipalter.com", "ipdefenseforum.com", @@ -3133,6 +3103,7 @@ var rules = [ "iphonetaiwan.org", "iphonix.fr", "ipicture.ru", + "ipify.org", "ipjetable.net", "ipobar.com", "ipoock.com", @@ -3142,9 +3113,10 @@ var rules = [ "iptv.com.tw", "iptvbin.com", "ipvanish.com", - "iqiyi.com", + "irangov.ir", "iredmail.org", "irib.ir", + "irna.ir", "ironpython.net", "ironsocket.com", "is-a-hunter.com", @@ -3182,6 +3154,7 @@ var rules = [ "itemdb.com", "itemfix.com", "ithome.com.tw", + "itiger.com", "itsaol.com", "itshidden.com", "itsky.it", @@ -3222,6 +3195,7 @@ var rules = [ "javakiba.org", "javbus.co", "javbus.com", + "javbus.sbs", "javdb.com", "javfinder.ai", "javfor.me", @@ -3236,6 +3210,7 @@ var rules = [ "javmoo.xyz", "javseen.com", "javtag.com", + "javtrailers.com", "javzoo.com", "javzz.com", "jbtalks.cc", @@ -3330,7 +3305,6 @@ var rules = [ "jwmusic.org", "jwplayer.com", "jyxf.net", - "k-doujin.net", "ka-wai.com", "kadokawa.co.jp", "kagyu.org", @@ -3379,7 +3353,6 @@ var rules = [ "kichiku-doujinko.com", "kik.com", "killwall.com", - "kimy.com.tw", "kindle4rss.com", "kindleren.com", "kingdomsalvation.org", @@ -3402,6 +3375,7 @@ var rules = [ "knowledgerush.com", "knowyourmeme.com", "ko-fi.com", + "kobe-np.co.jp", "kobo.com", "kobobooks.com", "kodingen.com", @@ -3416,6 +3390,7 @@ var rules = [ "kpkuang.org", "kqes.net", "kraken.com", + "krtc.com.tw", "krtco.com.tw", "ksdl.org", "ksnews.com.tw", @@ -3430,7 +3405,6 @@ var rules = [ "kurtmunger.com", "kusocity.com", "kwcg.ca", - "kwok7.com", "kwongwah.com.my", "kxsw.life", "kyofun.com", @@ -3459,7 +3433,6 @@ var rules = [ "laomiu.com", "laowang.vip", "laoyang.info", - "laptoplockdown.com", "laqingdan.net", "larsgeorge.com", "lastcombat.com", @@ -3469,6 +3442,8 @@ var rules = [ "lausan.hk", "law.com", "lbank.info", + "ldplayer.net", + "ldplayer.tw", "le-vpn.com", "leafyvpn.net", "lecloud.net", @@ -3485,15 +3460,11 @@ var rules = [ "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", @@ -3549,7 +3520,6 @@ var rules = [ "litenews.hk", "lithium.com", "liu-xiaobo.org", - "liudejun.com", "liuhanyu.com", "liujianshu.com", "liuxiaobo.net", @@ -3569,7 +3539,6 @@ var rules = [ "lkcn.net", "llss.me", "lmsys.org", - "lncn.org", "load.to", "lobsangwangyal.com", "localbitcoins.com", @@ -3608,6 +3577,8 @@ var rules = [ "lsxszzg.com", "ltn.com.tw", "luckydesigner.space", + "luckymobile.ca", + "ludepress.com", "luke54.com", "luke54.org", "lupm.org", @@ -3641,6 +3612,7 @@ var rules = [ "mail.ru", "mailchimp.com", "maildns.xyz", + "mainichi.jp", "maiplus.com", "maizhong.org", "makemymood.com", @@ -3689,17 +3661,17 @@ var rules = [ "matome-plus.com", "matome-plus.net", "matrix.org", - "matsushimakaede.com", "matters.news", "matters.town", "mattwilcox.net", - "maturejp.com", + "maxai.co", "maxing.jp", "mayimayi.com", "mcadforums.com", "mcaf.ee", "mcfog.com", "mcreasite.com", + "mcusercontent.com", "md-t.org", "me.com", "me.me", @@ -3754,6 +3726,7 @@ var rules = [ "metacafe.com", "metacubex.one", "metafilter.com", + "metamask.io", "metart.com", "metarthunter.com", "meteorshowersonline.com", @@ -3770,6 +3743,7 @@ var rules = [ "mh4u.org", "mhradio.org", "mi.com", + "miami-airport.com", "michaelmarketl.com", "microsoft.com", "microvpn.com", @@ -3808,6 +3782,7 @@ var rules = [ "miniforum.org", "miningpoolhub.com", "ministrybooks.org", + "minjian-danganguan.org", "minzhuhua.net", "minzhuzhanxian.com", "minzhuzhongguo.org", @@ -3818,6 +3793,7 @@ var rules = [ "mirrormedia.com.tw", "mirrormedia.mg", "missav.com", + "missav.ws", "mist.vip", "mit.edu", "mitao.com.tw", @@ -3849,18 +3825,18 @@ var rules = [ "moeerolibrary.com", "moegirl.org", "moeshare.cc", + "moeyy.xyz", "mofa.gov.tw", "mofaxiehui.com", "mofos.com", "mog.com", "mohu.club", - "mohu.ml", "mohu.rocks", + "moj.gov.tw", "mojim.com", "mol.gov.tw", "molihua.org", "momoshop.com.tw", - "monar.ch", "mondex.org", "money-link.com.tw", "moneydj.com", @@ -3886,7 +3862,6 @@ var rules = [ "mos.ru", "mosucloud.site", "motherless.com", - "motiyun.com", "motor4ik.ru", "mousebreaker.com", "movements.org", @@ -3912,6 +3887,7 @@ var rules = [ "msn.com.tw", "mstdn.social", "mswe1.org", + "mt.co.kr", "mthruf.com", "mtw.tl", "mtzfile.pw", @@ -3938,7 +3914,6 @@ var rules = [ "my-private-network.co.uk", "my-proxy.com", "my03.com", - "my903.com", "myactimes.com", "myanniu.com", "myaudiocast.com", @@ -3964,7 +3939,6 @@ var rules = [ "myfreepaysite.com", "myfreshnet.com", "myftp.info", - "myftp.name", "myip.com", "myiphide.com", "myiphider.com", @@ -3998,15 +3972,14 @@ var rules = [ "mywww.biz", "myz.info", "naacoalition.org", - "nabble.com", "naitik.net", + "naixi.net", "nakido.com", "nakuz.com", "nalandabodhi.org", "nalandawest.org", "namgyal.org", "namgyalmonastery.org", - "namsisi.com", "nanhuyt.com", "nanopool.org", "nanyang.com", @@ -4027,6 +4000,7 @@ var rules = [ "nationsonline.org", "nationwide.com", "naughtyamerica.com", + "naver.com", "naver.jp", "navy.mil", "naweeklytimes.com", @@ -4047,12 +4021,10 @@ var rules = [ "nekoslovakia.net", "nengcard.com", "neo-miracle.com", + "neoforged.net", "neowin.net", - "nepusoku.com", "nesnode.com", - "net-fits.pro", "netalert.me", - "netbig.com", "netbirds.com", "netcolony.com", "netfirms.com", @@ -4075,6 +4047,7 @@ var rules = [ "newchen.com", "newgrounds.com", "newhighlandvision.com", + "newindianexpress.com", "newipnow.com", "newlandmagazine.com.au", "newmitbbs.com", @@ -4118,27 +4091,29 @@ var rules = [ "nflximg.net", "nflxso.net", "nflxvideo.net", + "nftstorage.link", "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", + "nikke-en.com", + "nikke-jp.com", + "nikke-kr.com", "nikkei.com", "ninecommentaries.com", "ning.com", "ninjacloak.com", "ninjaproxy.ninja", "nintendium.com", - "ninth.biz", + "nirsoft.net", "nitter.cc", "nitter.net", "niu.moe", @@ -4156,7 +4131,6 @@ var rules = [ "nobodycanstop.us", "nodeseek.com", "nodesnoop.com", - "nofile.io", "nokogiri.org", "nokola.com", "noodlevpn.com", @@ -4168,6 +4142,7 @@ var rules = [ "nordstromrack.com", "nordvpn.com", "nos.nl", + "note.com", "notepad-plus-plus.org", "notion.site", "nottinghampost.com", @@ -4212,7 +4187,6 @@ var rules = [ "ntdtv.ru", "ntdtvla.com", "ntrfun.com", - "ntsna.gov.tw", "ntu.edu.tw", "nu.nl", "nubiles.net", @@ -4225,7 +4199,6 @@ var rules = [ "nutsvpn.work", "nuuvem.com", "nuvid.com", - "nuzcom.com", "nvdst.com", "nvquan.org", "nvtongzhisheng.org", @@ -4247,7 +4220,6 @@ var rules = [ "nytimes.com", "nytimes.map.fastly.net", "nytimg.com", - "nytlog.com", "nytstyle.com", "nzchinese.com", "nzchinese.net.nz", @@ -4285,10 +4257,10 @@ var rules = [ "okayfreedom.com", "okcoin.com", "okex.com", + "okinawatimes.co.jp", "okk.tw", "okpool.me", "okx.com", - "olabloga.pl", "old-cat.net", "olehdtv.com", "olelive.com", @@ -4326,21 +4298,20 @@ var rules = [ "onmypc.info", "onmypc.net", "onmypc.org", - "onmypc.us", "onthehunt.com", "ontrac.com", - "oopsforum.com", + "oojj.de", "open-assistant.io", "open.com.hk", "openai.com", "openallweb.com", "opendemocracy.net", + "opendesktop.org", "opendn.xyz", "openervpn.in", "openid.net", "openleaks.org", "opensea.io", - "opensource.google", "openstreetmap.org", "opentech.fund", "openvpn.net", @@ -4350,8 +4321,6 @@ var rules = [ "opera-mini.net", "opera.com", "opus-gaming.com", - "oraclecloud.com", - "orchidbbs.com", "organcare.org.tw", "organharvestinvestigation.net", "organiccrap.com", @@ -4361,9 +4330,9 @@ var rules = [ "orient-doll.com", "orientaldaily.com.my", "orn.jp", - "orzdream.com", "orzistic.org", "osfoora.com", + "osm.tw", "otcbtc.com", "otnd.org", "otto.de", @@ -4409,6 +4378,7 @@ var rules = [ "paljorpublications.com", "palmislife.com", "paltalk.com", + "pancakeswap.finance", "pandafan.pub", "pandapow.co", "pandapow.net", @@ -4432,7 +4402,6 @@ var rules = [ "partypoker.com", "passion.com", "passiontimes.hk", - "passwords.google", "paste.ee", "pastebin.com", "pastie.org", @@ -4462,7 +4431,6 @@ var rules = [ "peace.ca", "peacefire.org", "peacehall.com", - "pearlher.org", "peeasian.com", "peing.net", "pekingduck.org", @@ -4485,6 +4453,7 @@ var rules = [ "perplexity.ai", "persecutionblog.com", "persiankitty.com", + "pewresearch.org", "pfd.org.hk", "phapluan.org", "phayul.com", @@ -4502,6 +4471,7 @@ var rules = [ "picacomic.com", "picacomiccn.com", "picasaweb.com", + "picgo.net", "picidae.net", "picturedip.com", "picturesocial.com", @@ -4598,6 +4568,7 @@ var rules = [ "pornhub.com", "pornhubdeutsch.net", "pornhubpremium.com", + "pornmate.com", "pornmm.net", "pornoxo.com", "pornrapidshare.com", @@ -4616,7 +4587,6 @@ var rules = [ "post76.com", "post852.com", "postadult.com", - "postimg.org", "potato.im", "potatso.com", "potvpn.com", @@ -4629,20 +4599,23 @@ var rules = [ "pp.ru", "ppy.sh", "prayforchina.net", + "prcleader.org", "premeforwindows7.com", "premproxy.com", "presentation.new", "presentationzen.com", + "president.ir", "presidentlee.tw", + "pressreader.com", "prestige-av.com", "prettyvirgin.com", - "pride.google", "primevideo.com", "printfriendly.com", "prism-break.org", "prisoneralert.com", "pritunl.com", "privacybox.de", + "privacyguides.org", "private.com", "privateinternetaccess.com", "privatepaste.com", @@ -4668,7 +4641,6 @@ var rules = [ "proxydns.com", "proxylist.org.uk", "proxynetwork.org.uk", - "proxypy.net", "proxyroad.com", "proxytunnel.net", "proxz.com", @@ -4702,13 +4674,13 @@ var rules = [ "pure18.com", "pureapk.com", "pureconcepts.net", + "puredns.org", "pureinsight.org", "purepdf.com", "purevpn.com", "purplelotus.org", "pursuestar.com", "pushchinawall.com", - "pussthecat.org", "pussyspace.com", "putihome.org", "putlocker.com", @@ -4737,7 +4709,7 @@ var rules = [ "qiwen.lu", "qixianglu.cn", "qkshare.com", - "qmzdd.com", + "qmp4.com", "qoos.com", "qpoe.com", "qq.co.za", @@ -4745,7 +4717,6 @@ var rules = [ "qtrac.eu", "qtweeter.com", "quannengshen.org", - "quantumbooter.net", "questvisual.com", "quitccp.net", "quitccp.org", @@ -4776,10 +4747,10 @@ var rules = [ "radiotime.com", "radiovaticana.org", "radiovncr.com", + "radmin-vpn.com", "rael.org", "raggedbanner.com", "raidcall.com.tw", - "raidtalk.com.tw", "rainbowplan.org", "raindrop.io", "raizoji.or.jp", @@ -4803,11 +4774,9 @@ var rules = [ "ratx.com", "rawgit.com", "rawgithub.com", - "raxcdn.com", "razyboard.com", "rcinet.ca", "rd.com", - "rdio.com", "reabble.com", "read01.com", "read100.com", @@ -4832,6 +4801,7 @@ var rules = [ "redchinacn.org", "redd.it", "reddit.com", + "reddithelp.com", "redditlist.com", "redditmedia.com", "redditspace.com", @@ -4840,7 +4810,6 @@ var rules = [ "redtube.com", "referer.us", "reflectivecode.com", - "registry.google", "reimu.net", "relaxbbs.com", "relay.com.tw", @@ -4849,8 +4818,6 @@ var rules = [ "religioustolerance.org", "renminbao.com", "renyurenquan.org", - "rerouted.org", - "research.google", "resilio.com", "resistchina.org", "retweeteffect.com", @@ -4868,8 +4835,6 @@ var rules = [ "rferl.org", "rfi.fr", "rfi.my", - "rightbtc.com", - "rightster.com", "rigpa.org", "riku.me", "rileyguide.com", @@ -4900,7 +4865,6 @@ var rules = [ "rotten.com", "rou.video", "roucdn.link", - "rpglogs.com", "rsdlmonitor.com", "rsf-chinese.org", "rsf.org", @@ -4917,6 +4881,7 @@ var rules = [ "ruanyifeng.com", "rukor.org", "rule34.xxx", + "rule34video.com", "rumble.com", "runbtx.com", "rushbee.com", @@ -4932,9 +4897,15 @@ var rules = [ "s-nbcnews.com", "s1heng.com", "s1s1s1.com", + "s3-ap-northeast-1.amazonaws.com", + "s3-ap-northeast-2.amazonaws.com", "s3-ap-southeast-1.amazonaws.com", "s3-ap-southeast-2.amazonaws.com", + "s3-eu-central-1.amazonaws.com", "s3.amazonaws.com", + "s3.ap-northeast-2.amazonaws.com", + "s3.eu-central-1.amazonaws.com", + "s3.us-east-1.amazonaws.com", "s4miniarchive.com", "s8forum.com", "saboom.com", @@ -4946,10 +4917,8 @@ var rules = [ "safechat.com", "safeguarddefenders.com", "safervpn.com", - "safety.google", "sagernet.org", "saintyculture.com", - "saiq.me", "sakura-paris.org", "sakuralive.com", "sakya.org", @@ -4974,7 +4943,6 @@ var rules = [ "savetibetstore.org", "saveuighur.org", "savevid.com", - "say2.info", "sbme.me", "sbs.com.au", "scasino.com", @@ -4982,6 +4950,7 @@ var rules = [ "sciencemag.org", "sciencenets.com", "scieron.com", + "sclub.com.tw", "scmp.com", "scmpchinese.com", "scramble.io", @@ -5023,7 +4992,6 @@ var rules = [ "sethwklein.net", "setn.com", "settv.com.tw", - "setty.com.tw", "sevenload.com", "sex-11.com", "sex.com", @@ -5036,7 +5004,6 @@ var rules = [ "sexidude.com", "sexinsex.net", "sextvx.com", - "sexxxy.biz", "sf.net", "sfileydy.com", "sfshibao.com", @@ -5054,7 +5021,6 @@ var rules = [ "shadowsocks.com.hk", "shadowsocks.nu", "shadowsocks.org", - "shadowsocks9.com", "shafaqna.com", "shahit.biz", "shambalapost.com", @@ -5086,7 +5052,6 @@ var rules = [ "shicheng.org", "shiksha.com", "shiksha.ws", - "shinychan.com", "shipcamouflage.com", "shireyishunjian.com", "shitaotv.org", @@ -5102,7 +5067,6 @@ var rules = [ "showtime.jp", "showwe.tw", "shutterstock.com", - "shvoong.com", "shwchurch.org", "shwchurch3.com", "siddharthasintent.org", @@ -5119,9 +5083,9 @@ var rules = [ "simplecd.org", "simpleproductivityblog.com", "simpleswap.io", + "simplex.chat", "sina.com", "sina.com.hk", - "sina.com.tw", "sinchew.com.my", "singaporepools.com.sg", "singfortibet.com", @@ -5209,7 +5173,6 @@ var rules = [ "soc.mil", "social.edu.ci", "socialblade.com", - "socialwhale.com", "socks-proxy.net", "sockscap64.com", "sockslist.net", @@ -5220,9 +5183,8 @@ var rules = [ "softether.org", "softfamous.com", "softlayer.net", - "softnology.biz", "softonic.cn", - "softsmirror.cf", + "softonic.com", "softwarebychuck.com", "sogclub.com", "sogoo.org", @@ -5256,7 +5218,6 @@ var rules = [ "soubory.com", "soul-plus.net", "soulcaliburhentai.net", - "soumo.info", "soundcloud.com", "soundofhope.kr", "soundofhope.org", @@ -5325,6 +5286,7 @@ var rules = [ "stackoverflow.com", "stacksocial.com", "stage64.hk", + "standard.co.uk", "standupfortibet.org", "standwithhk.org", "stanford.edu", @@ -5334,6 +5296,7 @@ var rules = [ "startuplivingchina.com", "stat.gov.tw", "state.gov", + "statearmor.org", "static-economist.com", "statically.io", "staticflickr.com", @@ -5361,11 +5324,11 @@ var rules = [ "stoporganharvesting.org", "stoptibetcrisis.net", "storagenewsletter.com", - "stories.google", "storify.com", "storj.io", "storm.mg", "stormmediagroup.com", + "storry.tv", "stoweboyd.com", "straitstimes.com", "stranabg.com", @@ -5382,9 +5345,11 @@ var rules = [ "strongwindpress.com", "student.tw", "studentsforafreetibet.org", + "studybuddhism.com", "stumbleupon.com", "stupidvideos.com", "stweetly.com", + "subhd.tv", "substack.com", "successfn.com", "sueddeutsche.de", @@ -5392,7 +5357,6 @@ var rules = [ "sugobbs.com", "sugumiru18.com", "suissl.com", - "sulian.me", "summify.com", "sumrando.com", "sun1911.com", @@ -5407,7 +5371,6 @@ var rules = [ "suoluo.org", "supchina.com", "superfreevpn.com", - "superokayama.com", "superpages.com", "supervpn.net", "superzooi.com", @@ -5419,7 +5382,6 @@ var rules = [ "surfshark.com", "suroot.com", "surrenderat20.net", - "sustainability.google", "suyangg.com", "svsfx.com", "swagbucks.com", @@ -5437,7 +5399,6 @@ var rules = [ "syosetu.com", "sysresccd.org", "sytes.net", - "syx86.cn", "syx86.com", "szbbs.net", "szetowah.org.hk", @@ -5484,6 +5445,7 @@ var rules = [ "taiwanus.net", "taiwanyes.com", "talk853.com", + "talkatone.com", "talkboxapp.com", "talkcc.com", "talkonly.net", @@ -5492,6 +5454,7 @@ var rules = [ "tanc.org", "tangben.com", "tangren.us", + "tanks.gg", "taoism.net", "taolun.info", "tapanwap.com", @@ -5505,13 +5468,11 @@ var rules = [ "taweet.com", "tbcollege.org", "tbi.org.hk", - "tbicn.org", "tbjyt.org", - "tbpic.info", "tbrc.org", "tbs-rainbow.org", + "tbs.co.jp", "tbsec.org", - "tbsmalaysia.org", "tbsn.org", "tbsseattle.org", "tbssqh.org", @@ -5544,8 +5505,8 @@ var rules = [ "teepr.com", "tehrantimes.com", "telecomspace.com", + "telega.one", "telegra.ph", - "telegram-cdn.org", "telegram.dog", "telegram.me", "telegram.org", @@ -5553,6 +5514,7 @@ var rules = [ "telegramdownload.com", "telegraph.co.uk", "telesco.pe", + "tellapart.com", "tellme.pw", "tenacy.com", "tenor.com", @@ -5566,6 +5528,7 @@ var rules = [ "tfhub.dev", "tfiflve.com", "tg-me.com", + "tg.dev", "tgstat.com", "thaicn.com", "thb.gov.tw", @@ -5590,11 +5553,11 @@ var rules = [ "thedw.us", "theepochtimes.com", "thefacebook.com", - "thefrontier.hk", "thegay.com", "thegioitinhoc.vn", "thegly.com", "theguardian.com", + "thehansindia.com", "thehindu.com", "thehots.info", "thehousenews.com", @@ -5606,7 +5569,6 @@ var rules = [ "theporndude.com", "theportalwiki.com", "theprint.in", - "thereallove.kr", "therock.net.nz", "thesaturdaypaper.com.au", "thespeeder.com", @@ -5620,7 +5582,6 @@ var rules = [ "thetibetpost.com", "thetrotskymovie.com", "thetvdb.com", - "thevivekspot.com", "thewgo.org", "thewirechina.com", "theync.com", @@ -5634,6 +5595,7 @@ var rules = [ "thomasbernhard.org", "thongdreams.com", "threadreaderapp.com", + "threads.com", "threads.net", "threatchaos.com", "throughnightsfire.com", @@ -5738,10 +5700,12 @@ var rules = [ "ticket.com.tw", "tigervpn.com", "tiktok.com", + "tiktokcdn-eu.com", "tiktokcdn-us.com", "tiktokcdn.com", "tiktokv.com", "tiktokv.us", + "tiktokw.us", "tiltbrush.com", "timdir.com", "time.com", @@ -5752,7 +5716,6 @@ var rules = [ "tiney.com", "tineye.com", "tingtalk.me", - "tintuc101.com", "tiny.cc", "tinychat.com", "tinypaste.com", @@ -5764,13 +5727,11 @@ var rules = [ "tl.gd", "tma.co.jp", "tmagazine.com", - "tmdfish.com", "tmi.me", "tmpp.org", "tnaflix.com", - "tngrnow.com", - "tngrnow.net", "tnp.org", + "tnt-ea.com", "to-porno.com", "togetter.com", "toh.info", @@ -5783,7 +5744,6 @@ var rules = [ "tomonews.net", "tomp3.cc", "tongil.or.kr", - "tono-oka.jp", "tonyyan.net", "toodoc.com", "toonel.net", @@ -5867,7 +5827,6 @@ var rules = [ "tuidang.net", "tuidang.org", "tuidang.se", - "tuitui.info", "tuitwit.com", "tukaani.org", "tumblr.com", @@ -5896,11 +5855,9 @@ var rules = [ "turkistantimes.com", "turntable.fm", "tushycash.com", - "tutanota.com", "tuvpn.com", "tuzaijidi.com", "tv.com", - "tv.google", "tvants.com", "tvb.com", "tvboxnow.com", @@ -5968,11 +5925,8 @@ var rules = [ "twisternow.com", "twistory.net", "twit2d.com", - "twitbrowser.net", - "twitcause.com", "twitch.tv", "twitchcdn.net", - "twitgether.com", "twitgoo.com", "twitiq.com", "twitlonger.com", @@ -6009,6 +5963,7 @@ var rules = [ "twttr.com", "twurl.nl", "twyac.org", + "tx.me", "txxx.com", "tycool.com", "typekit.net", @@ -6031,6 +5986,7 @@ var rules = [ "udn.com.tw", "udnbkk.com", "udndata.com", + "udomain.hk", "uforadio.com.tw", "ufreevpn.com", "ugo.com", @@ -6077,6 +6033,7 @@ var rules = [ "untraceable.us", "unwire.hk", "uocn.org", + "upbit.com", "updatestar.com", "upghsbc.com", "upholdjustice.org", @@ -6130,7 +6087,6 @@ var rules = [ "uyghuraa.org", "uyghuramerican.org", "uyghurbiz.org", - "uyghurcanadian.ca", "uyghurcanadiansociety.org", "uyghurcongress.org", "uyghurensemble.co.uk", @@ -6148,7 +6104,6 @@ var rules = [ "v2ray.com", "v2raya.org", "v2raycn.com", - "v2raytech.com", "valeursactuelles.com", "van001.com", "van698.com", @@ -6203,7 +6158,6 @@ var rules = [ "virginia.edu", "virtualrealporn.com", "visibletweets.com", - "visiontimes.com", "visualstudio.com", "vital247.org", "viu.com", @@ -6283,12 +6237,12 @@ var rules = [ "vpnworldwide.com", "vporn.com", "vpser.net", + "vpsxb.net", "vraiesagesse.net", "vrchat.com", "vrmtr.com", "vrporn.com", "vrsmash.com", - "vs.com", "vtunnel.com", "vuku.cc", "vultryhw.com", @@ -6297,6 +6251,7 @@ var rules = [ "w-pool.com", "w.wiki", "w3.org", + "w3s.link", "waffle1999.com", "wahas.com", "waigaobu.com", @@ -6304,6 +6259,7 @@ var rules = [ "wailaike.net", "wainao.me", "waiwaier.com", + "walletconnect.com", "wallhaven.cc", "wallmama.com", "wallornot.org", @@ -6323,7 +6279,6 @@ var rules = [ "want-daily.com", "wanz-factory.com", "wapedia.mobi", - "warehouse333.com", "warroom.org", "waselpro.com", "washeng.net", @@ -6364,7 +6319,6 @@ var rules = [ "webworkerdaily.com", "wechatlawsuit.com", "weebly.com", - "weekmag.info", "wefightcensorship.org", "wefong.com", "weiboleak.com", @@ -6380,7 +6334,6 @@ var rules = [ "wengewang.com", "wengewang.org", "wenhui.ch", - "wenweipo.com", "wenxuecity.com", "wenyunchao.com", "wenzhao.ca", @@ -6395,7 +6348,6 @@ var rules = [ "wezhiyong.org", "wezone.net", "wforum.com", - "wha.la", "whatblocked.com", "whatbrowser.org", "whats.new", @@ -6446,9 +6398,7 @@ var rules = [ "williamhill.com", "willw.net", "wilsoncenter.org", - "windowsphoneme.com", "windscribe.com", - "windy.com", "wingamestore.com", "wingy.site", "winning11.com", @@ -6488,7 +6438,6 @@ var rules = [ "workerempowerment.org", "workers.dev", "workersthebig.net", - "workflow.is", "worldcat.org", "worldjournal.com", "worldpopulationreview.com", @@ -6529,6 +6478,8 @@ var rules = [ "wwitv.com", "www1.biz", "wwwhost.biz", + "wxw.cat", + "wxw.moe", "wzyboy.im", "x-art.com", "x-berry.com", @@ -6569,6 +6520,7 @@ var rules = [ "xiaoma.org", "xiaomi.eu", "xiaxiaoqiang.net", + "xicons.org", "xiezhua.com", "xihua.es", "xinbao.de", @@ -6581,7 +6533,6 @@ var rules = [ "xinyubbs.net", "xiongpian.com", "xiuren.org", - "xixicui.icu", "xizang-zhiye.org", "xjp.cc", "xjtravelguide.com", @@ -6610,7 +6561,6 @@ var rules = [ "xsden.info", "xsden.org", "xskywalker.com", - "xskywalker.net", "xt.com", "xt.pub", "xtube.com", @@ -6633,7 +6583,6 @@ var rules = [ "xxx.xxx", "xxxfuckmom.com", "xxxx.com.au", - "xxxy.biz", "xxxy.info", "xxxymovies.com", "xys.org", @@ -6647,6 +6596,7 @@ var rules = [ "yahoo.com.hk", "yahoo.com.tw", "yahoo.net", + "yahooinc.com", "yahoosandbox.com", "yakbutterblues.com", "yam.com", @@ -6657,6 +6607,7 @@ var rules = [ "yandex.ru", "yanghengjun.com", "yangjianli.com", + "yangzhi.org", "yasni.co.uk", "yasukuni.or.jp", "yayabay.com", @@ -6686,9 +6637,11 @@ var rules = [ "yinlei.org", "yipub.com", "yizhihongxing.com", + "ylive.jp", "yobit.net", "yobt.com", "yobt.tv", + "yodobashi.com", "yogichen.org", "yolasite.com", "yomiuri.co.jp", @@ -6713,7 +6666,6 @@ var rules = [ "yourtrap.com", "yousendit.com", "youshun12.com", - "youthforfreechina.org", "youthnetradio.org", "youthwant.com.tw", "youtu.be", @@ -6748,8 +6700,13 @@ var rules = [ "yyjlymb.xyz", "yysub.net", "yzzk.com", + "z-lib.fm", + "z-lib.fo", + "z-lib.gd", + "z-lib.gl", "z-lib.io", "z-lib.org", + "z-library.sk", "zacebook.com", "zalmos.com", "zamimg.com", @@ -6786,11 +6743,9 @@ var rules = [ "zhongguo.ca", "zhongguorenquan.org", "zhongguotese.net", - "zhongmeng.org", "zhongzidi.com", "zhoushuguang.com", "zhreader.com", - "zhuangbi.me", "zhuanxing.cn", "zhuatieba.com", "zhuichaguoji.org", @@ -6808,6 +6763,7 @@ var rules = [ "zmedia.com.tw", "zmw.cn", "zodgame.us", + "zodgame.xyz", "zoho.com", "zomobo.net", "zonaeuropa.com", @@ -6829,11 +6785,9 @@ var rules = [ "zuobiao.me", "zuola.com", "zvereff.com", - "zynaima.com", "zynamics.com", "zyns.com", "zyxel.com", - "zyzc9.com", "zzcartoon.com", "zzcloud.me", "zzux.com" diff --git a/v2rayN/ServiceLib/Sample/singbox_fakeip_filter b/v2rayN/ServiceLib/Sample/singbox_fakeip_filter index 860ab0eb..8095848e 100644 --- a/v2rayN/ServiceLib/Sample/singbox_fakeip_filter +++ b/v2rayN/ServiceLib/Sample/singbox_fakeip_filter @@ -1,4 +1,4 @@ -{ +{ "domain": [ "amobile.music.tc.qq.com", "api-jooxtt.sanook.com", @@ -28,6 +28,7 @@ "swdownload.apple.com", "swquery.apple.com", "swscan.apple.com", + "turn.cloudflare.com", "trackercdn.kugou.com", "xnotify.xboxlive.com" ], diff --git a/v2rayN/ServiceLib/ServiceLib.csproj b/v2rayN/ServiceLib/ServiceLib.csproj index dde35dc3..8cfbf8ca 100644 --- a/v2rayN/ServiceLib/ServiceLib.csproj +++ b/v2rayN/ServiceLib/ServiceLib.csproj @@ -11,7 +11,7 @@ - + @@ -57,6 +57,9 @@ Designer PublicResXFileCodeGenerator + + PublicResXFileCodeGenerator + PublicResXFileCodeGenerator diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs index e102f17d..b9fcc126 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigClashService.cs @@ -79,6 +79,7 @@ public class CoreConfigClashService //external-controller fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}"; + fileContent.Remove("secret"); //allow-lan if (_config.Inbound.First().AllowLANConn) { diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs index 4e24d8e2..71f25acd 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs @@ -1,6 +1,3 @@ -using System.Net; -using System.Net.NetworkInformation; - namespace ServiceLib.Services.CoreConfig; public partial class CoreConfigSingboxService(Config config) @@ -16,7 +13,7 @@ public partial class CoreConfigSingboxService(Config config) try { if (node == null - || node.Port <= 0) + || !node.IsValid()) { ret.Msg = ResUI.CheckServerSettings; return ret; @@ -29,6 +26,18 @@ public partial class CoreConfigSingboxService(Config config) ret.Msg = ResUI.InitialConfiguration; + if (node.ConfigType.IsGroupType()) + { + switch (node.ConfigType) + { + case EConfigType.PolicyGroup: + return await GenerateClientMultipleLoadConfig(node); + + case EConfigType.ProxyChain: + return await GenerateClientChainConfig(node); + } + } + var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient); if (result.IsNullOrEmpty()) { @@ -142,12 +151,9 @@ public partial class CoreConfigSingboxService(Config config) continue; } var item = await AppManager.Instance.GetProfileItem(it.IndexId); - if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) + if (item is null || item.IsComplex() || !item.IsValid()) { - if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) - { - continue; - } + continue; } //find unused port @@ -187,27 +193,6 @@ public partial class CoreConfigSingboxService(Config config) 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) { @@ -246,7 +231,7 @@ public partial class CoreConfigSingboxService(Config config) } singboxConfig.route.default_domain_resolver = new() { - server = Global.SingboxFinalResolverTag + server = Global.SingboxLocalDNSTag, }; ret.Success = true; @@ -266,7 +251,8 @@ public partial class CoreConfigSingboxService(Config config) var ret = new RetResult(); try { - if (node is not { Port: > 0 }) + if (node == null + || !node.IsValid()) { ret.Msg = ResUI.CheckServerSettings; return ret; @@ -318,7 +304,7 @@ public partial class CoreConfigSingboxService(Config config) } singboxConfig.route.default_domain_resolver = new() { - server = Global.SingboxFinalResolverTag + server = Global.SingboxLocalDNSTag, }; singboxConfig.route.rules.Clear(); @@ -344,7 +330,7 @@ public partial class CoreConfigSingboxService(Config config) } } - public async Task GenerateClientMultipleLoadConfig(List selecteds) + public async Task GenerateClientMultipleLoadConfig(ProfileItem parentNode) { var ret = new RetResult(); try @@ -371,56 +357,77 @@ public partial class CoreConfigSingboxService(Config config) ret.Msg = ResUI.FailedGenDefaultConfiguration; return ret; } + singboxConfig.outbounds.RemoveAt(0); 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) + var groupRet = await GenGroupOutbound(parentNode, singboxConfig); + if (groupRet != 0) { ret.Msg = ResUI.FailedGenDefaultConfiguration; return ret; } - await GenOutboundsList(proxyProfiles, singboxConfig); + await GenRouting(singboxConfig); + await GenExperimental(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 GenerateClientChainConfig(ProfileItem parentNode) + { + 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; + } + singboxConfig.outbounds.RemoveAt(0); + + await GenLog(singboxConfig); + await GenInbounds(singboxConfig); + + var groupRet = await GenGroupOutbound(parentNode, singboxConfig); + if (groupRet != 0) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + await GenRouting(singboxConfig); + await GenExperimental(singboxConfig); await GenDns(null, singboxConfig); await ConvertGeo2Ruleset(singboxConfig); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs index c6bec22b..6fe9b35a 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Nodes; - namespace ServiceLib.Services.CoreConfig; public partial class CoreConfigSingboxService diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index 00bb14d9..f6190ea9 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -43,16 +43,7 @@ public partial class CoreConfigSingboxService }); } - // 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], - }); - } + await GenOutboundDnsRule(node, singboxConfig); } catch (Exception ex) { @@ -67,16 +58,12 @@ public partial class CoreConfigSingboxService var directDns = ParseDnsAddress(simpleDNSItem.DirectDNS); directDns.tag = Global.SingboxDirectDNSTag; - directDns.domain_resolver = Global.SingboxFinalResolverTag; + directDns.domain_resolver = Global.SingboxLocalDNSTag; 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; + remoteDns.domain_resolver = Global.SingboxLocalDNSTag; var hostsDns = new Server4Sbox { @@ -121,10 +108,6 @@ public partial class CoreConfigSingboxService { 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; @@ -135,7 +118,6 @@ public partial class CoreConfigSingboxService 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 @@ -156,8 +138,8 @@ public partial class CoreConfigSingboxService private async Task GenDnsDomains(SingboxConfig singboxConfig, SimpleDNSItem? simpleDNSItem) { - var finalDns = ParseDnsAddress(simpleDNSItem.SingboxFinalResolveDNS); - finalDns.tag = Global.SingboxFinalResolverTag; + var finalDns = ParseDnsAddress(simpleDNSItem.BootstrapDNS); + finalDns.tag = Global.SingboxLocalDNSTag; singboxConfig.dns ??= new Dns4Sbox(); singboxConfig.dns.servers ??= new List(); singboxConfig.dns.servers.Add(finalDns); @@ -220,7 +202,9 @@ public partial class CoreConfigSingboxService var routing = await ConfigHandler.GetDefaultRouting(_config); if (routing == null) + { return 0; + } var rules = JsonUtils.Deserialize>(routing.RuleSet) ?? []; var expectedIPCidr = new List(); @@ -262,6 +246,11 @@ public partial class CoreConfigSingboxService continue; } + if (item.RuleType == ERuleType.Routing) + { + continue; + } + var rule = new Rule4Sbox(); var validDomains = item.Domain.Count(it => ParseV2Domain(it, rule)); if (validDomains <= 0) @@ -346,16 +335,7 @@ public partial class CoreConfigSingboxService 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], - }); - } + await GenOutboundDnsRule(node, singboxConfig); } catch (Exception ex) { @@ -364,16 +344,17 @@ public partial class CoreConfigSingboxService return 0; } - private async Task GenDnsDomainsCompatible(SingboxConfig singboxConfig, DNSItem? dNSItem) + 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 tag = Global.SingboxLocalDNSTag; - var localDnsServer = ParseDnsAddress(localDnsAddress); + var finalDnsAddress = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress; + + var localDnsServer = ParseDnsAddress(finalDnsAddress); localDnsServer.tag = tag; dns4Sbox.servers.Add(localDnsServer); @@ -382,19 +363,19 @@ public partial class CoreConfigSingboxService return await Task.FromResult(0); } - private async Task GenDnsDomainsLegacyCompatible(SingboxConfig singboxConfig, DNSItem? dNSItem) + private async Task GenDnsDomainsLegacyCompatible(SingboxConfig singboxConfig, DNSItem? dnsItem) { var dns4Sbox = singboxConfig.dns ?? new(); dns4Sbox.servers ??= []; dns4Sbox.rules ??= []; - var tag = Global.SingboxFinalResolverTag; + var tag = Global.SingboxLocalDNSTag; dns4Sbox.servers.Add(new() { tag = tag, - address = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dNSItem?.DomainDNSAddress, + address = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress, detour = Global.DirectTag, - strategy = string.IsNullOrEmpty(dNSItem?.DomainStrategy4Freedom) ? null : dNSItem?.DomainStrategy4Freedom, + strategy = string.IsNullOrEmpty(dnsItem?.DomainStrategy4Freedom) ? null : dnsItem?.DomainStrategy4Freedom, }); dns4Sbox.rules.Insert(0, new() { @@ -425,6 +406,40 @@ public partial class CoreConfigSingboxService return await Task.FromResult(0); } + private async Task GenOutboundDnsRule(ProfileItem? node, SingboxConfig singboxConfig) + { + if (node == null) + { + return 0; + } + + List domain = new(); + if (Utils.IsDomain(node.Address)) // normal outbound + { + domain.Add(node.Address); + } + if (node.Address == Global.Loopback && node.SpiderX.IsNotEmpty()) // Tun2SocksAddress + { + domain.AddRange(Utils.String2List(node.SpiderX) + .Where(Utils.IsDomain) + .Distinct() + .ToList()); + } + if (domain.Count == 0) + { + return 0; + } + + singboxConfig.dns.rules ??= new List(); + singboxConfig.dns.rules.Insert(0, new Rule4Sbox + { + server = Global.SingboxLocalDNSTag, + domain = domain, + }); + + return await Task.FromResult(0); + } + private static Server4Sbox? ParseDnsAddress(string address) { var addressFirst = address?.Split(address.Contains(',') ? ',' : ';').FirstOrDefault()?.Trim(); @@ -441,79 +456,41 @@ public partial class CoreConfigSingboxService return server; } - if (addressFirst.StartsWith("dhcp://", StringComparison.OrdinalIgnoreCase)) + var (domain, scheme, port, path) = Utils.ParseUrl(addressFirst); + + if (scheme.Equals("dhcp", StringComparison.OrdinalIgnoreCase)) { - var interface_name = addressFirst.Substring(7); server.type = "dhcp"; - server.Interface = interface_name == "auto" ? null : interface_name; + if ((!domain.IsNullOrEmpty()) && domain != "auto") + { + server.server = domain; + } return server; } - if (!addressFirst.Contains("://")) + if (scheme.IsNullOrEmpty()) { // udp dns server.type = "udp"; - server.server = addressFirst; - return server; } - - try + else { - var protocolEndIndex = addressFirst.IndexOf("://", StringComparison.Ordinal); - server.type = addressFirst.Substring(0, protocolEndIndex).ToLower(); + // server.type = scheme.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; - } + // remove "+local" suffix + // TODO: "+local" suffix decide server.detour = "direct" ? + server.type = scheme.Replace("+local", "", StringComparison.OrdinalIgnoreCase).ToLower(); } - catch (UriFormatException) + + server.server = domain; + if (port != 0) { - 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); - } - } + server.server_port = port; + } + if ((server.type == "https" || server.type == "h3") && !string.IsNullOrEmpty(path) && path != "/") + { + server.path = path; } - return server; } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs index 91f76ad9..76e90110 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs @@ -61,7 +61,7 @@ public partial class CoreConfigSingboxService } var tunInbound = JsonUtils.Deserialize(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { }; - tunInbound.interface_name = Utils.IsOSX() ? $"utun{new Random().Next(99)}" : "singbox_tun"; + tunInbound.interface_name = Utils.IsMacOS() ? $"utun{new Random().Next(99)}" : "singbox_tun"; tunInbound.mtu = _config.TunModeItem.Mtu; tunInbound.auto_route = _config.TunModeItem.AutoRoute; tunInbound.strict_route = _config.TunModeItem.StrictRoute; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs index 3f03b93c..9671dc77 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs @@ -179,13 +179,21 @@ public partial class CoreConfigSingboxService if (node.ConfigType == EConfigType.WireGuard) { var endpoint = JsonUtils.Deserialize(txtOutbound); - await GenEndpoint(node, endpoint); + var ret = await GenEndpoint(node, endpoint); + if (ret != 0) + { + return null; + } return endpoint; } else { var outbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(node, outbound); + var ret = await GenOutbound(node, outbound); + if (ret != 0) + { + return null; + } return outbound; } } @@ -224,7 +232,7 @@ public partial class CoreConfigSingboxService { try { - if (node.StreamSecurity == Global.StreamSecurityReality || node.StreamSecurity == Global.StreamSecurity) + if (node.StreamSecurity is Global.StreamSecurityReality or Global.StreamSecurity) { var server_name = string.Empty; if (node.Sni.IsNotEmpty()) @@ -238,7 +246,7 @@ public partial class CoreConfigSingboxService var tls = new Tls4Sbox() { enabled = true, - record_fragment = _config.CoreBasicItem.EnableFragment, + record_fragment = _config.CoreBasicItem.EnableFragment ? true : null, server_name = server_name, insecure = Utils.ToBool(node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : node.AllowInsecure), alpn = node.GetAlpn(), @@ -251,7 +259,16 @@ public partial class CoreConfigSingboxService fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint }; } - if (node.StreamSecurity == Global.StreamSecurityReality) + if (node.StreamSecurity == Global.StreamSecurity) + { + var certs = CertPemManager.ParsePemChain(node.Cert); + if (certs.Count > 0) + { + tls.certificate = certs; + tls.insecure = false; + } + } + else if (node.StreamSecurity == Global.StreamSecurityReality) { tls.reality = new Reality4Sbox() { @@ -348,6 +365,54 @@ public partial class CoreConfigSingboxService return await Task.FromResult(0); } + private async Task GenGroupOutbound(ProfileItem node, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false) + { + try + { + if (!node.ConfigType.IsGroupType()) + { + return -1; + } + var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId); + if (hasCycle) + { + return -1; + } + + var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + if (childProfiles.Count <= 0) + { + return -1; + } + switch (node.ConfigType) + { + case EConfigType.PolicyGroup: + if (ignoreOriginChain) + { + await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName); + } + else + { + await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName); + } + + break; + + case EConfigType.ProxyChain: + await GenChainOutboundsList(childProfiles, singboxConfig, baseTagName); + break; + + default: + break; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + private async Task GenMoreOutbounds(ProfileItem node, SingboxConfig singboxConfig) { if (node.Subid.IsNullOrEmpty()) @@ -410,7 +475,7 @@ public partial class CoreConfigSingboxService return 0; } - private async Task GenOutboundsList(List nodes, SingboxConfig singboxConfig) + private async Task GenOutboundsListWithChain(List nodes, SingboxConfig singboxConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag) { try { @@ -438,6 +503,29 @@ public partial class CoreConfigSingboxService { index++; + if (node.ConfigType.IsGroupType()) + { + var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + if (childProfiles.Count <= 0) + { + continue; + } + var childBaseTagName = $"{baseTagName}-{index}"; + var ret = node.ConfigType switch + { + EConfigType.PolicyGroup => + await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName), + EConfigType.ProxyChain => + await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName), + _ => throw new NotImplementedException() + }; + if (ret == 0) + { + proxyTags.Add(childBaseTagName); + } + continue; + } + // Handle proxy chain string? prevTag = null; var currentServer = await GenServer(node); @@ -450,7 +538,7 @@ public partial class CoreConfigSingboxService var subItem = await AppManager.Instance.GetSubItem(node.Subid); // current proxy - currentServer.tag = $"{Global.ProxyTag}-{index}"; + currentServer.tag = $"{baseTagName}-{index}"; proxyTags.Add(currentServer.tag); if (!node.Subid.IsNullOrEmpty()) @@ -467,7 +555,7 @@ public partial class CoreConfigSingboxService { var prevOutbound = JsonUtils.Deserialize(txtOutbound); await GenOutbound(prevNode, prevOutbound); - prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}"; + prevTag = $"prev-{baseTagName}-{++prevIndex}"; prevOutbound.tag = prevTag; prevOutbounds.Add(prevOutbound); } @@ -508,16 +596,21 @@ public partial class CoreConfigSingboxService var outUrltest = new Outbound4Sbox { type = "urltest", - tag = $"{Global.ProxyTag}-auto", + tag = $"{baseTagName}-auto", outbounds = proxyTags, interrupt_exist_connections = false, }; + if (multipleLoad == EMultipleLoad.Fallback) + { + outUrltest.tolerance = 5000; + } + // Add selector outbound (manual selection) var outSelector = new Outbound4Sbox { type = "selector", - tag = Global.ProxyTag, + tag = baseTagName, outbounds = JsonUtils.DeepCopy(proxyTags), interrupt_exist_connections = false, }; @@ -529,12 +622,12 @@ public partial class CoreConfigSingboxService } // 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; + var serverList = new List(); + serverList = serverList.Concat(prevOutbounds) + .Concat(resultOutbounds) + .Concat(resultEndpoints) + .ToList(); + await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag); } catch (Exception ex) { @@ -574,4 +667,166 @@ public partial class CoreConfigSingboxService } return null; } + + private async Task GenOutboundsList(List nodes, SingboxConfig singboxConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag) + { + var resultOutbounds = new List(); + var resultEndpoints = new List(); // For endpoints + var proxyTags = new List(); // For selector and urltest outbounds + for (var i = 0; i < nodes.Count; i++) + { + var node = nodes[i]; + if (node == null) + { + continue; + } + + if (node.ConfigType.IsGroupType()) + { + var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + if (childProfiles.Count <= 0) + { + continue; + } + var childBaseTagName = $"{baseTagName}-{i + 1}"; + var ret = node.ConfigType switch + { + EConfigType.PolicyGroup => + await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName), + EConfigType.ProxyChain => + await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName), + _ => throw new NotImplementedException() + }; + if (ret == 0) + { + proxyTags.Add(childBaseTagName); + } + continue; + } + var server = await GenServer(node); + if (server is null) + { + break; + } + server.tag = baseTagName + (i + 1).ToString(); + if (server is Endpoints4Sbox endpoint) + { + resultEndpoints.Add(endpoint); + } + else if (server is Outbound4Sbox outbound) + { + resultOutbounds.Add(outbound); + } + proxyTags.Add(server.tag); + } + // Add urltest outbound (auto selection based on latency) + if (proxyTags.Count > 0) + { + var outUrltest = new Outbound4Sbox + { + type = "urltest", + tag = $"{baseTagName}-auto", + outbounds = proxyTags, + interrupt_exist_connections = false, + }; + if (multipleLoad == EMultipleLoad.Fallback) + { + outUrltest.tolerance = 5000; + } + // Add selector outbound (manual selection) + var outSelector = new Outbound4Sbox + { + type = "selector", + tag = baseTagName, + 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); + } + var serverList = new List(); + serverList = serverList.Concat(resultOutbounds) + .Concat(resultEndpoints) + .ToList(); + await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag); + return await Task.FromResult(0); + } + + private async Task GenChainOutboundsList(List nodes, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag) + { + // Based on actual network flow instead of data packets + var nodesReverse = nodes.AsEnumerable().Reverse().ToList(); + var resultOutbounds = new List(); + var resultEndpoints = new List(); // For endpoints + for (var i = 0; i < nodesReverse.Count; i++) + { + var node = nodesReverse[i]; + var server = await GenServer(node); + + if (server is null) + { + break; + } + + if (i == 0) + { + server.tag = baseTagName; + } + else + { + server.tag = baseTagName + i.ToString(); + } + + if (i != nodesReverse.Count - 1) + { + server.detour = baseTagName + (i + 1).ToString(); + } + + if (server is Endpoints4Sbox endpoint) + { + resultEndpoints.Add(endpoint); + } + else if (server is Outbound4Sbox outbound) + { + resultOutbounds.Add(outbound); + } + } + var serverList = new List(); + serverList = serverList.Concat(resultOutbounds) + .Concat(resultEndpoints) + .ToList(); + await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag); + return await Task.FromResult(0); + } + + private async Task AddRangeOutbounds(List servers, SingboxConfig singboxConfig, bool prepend = true) + { + try + { + if (servers is null || servers.Count <= 0) + { + return 0; + } + var outbounds = servers.Where(s => s is Outbound4Sbox).Cast().ToList(); + var endpoints = servers.Where(s => s is Endpoints4Sbox).Cast().ToList(); + singboxConfig.endpoints ??= new(); + if (prepend) + { + singboxConfig.outbounds.InsertRange(0, outbounds); + singboxConfig.endpoints.InsertRange(0, endpoints); + } + else + { + singboxConfig.outbounds.AddRange(outbounds); + singboxConfig.endpoints.AddRange(endpoints); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index 21dfb101..33fde24e 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -9,13 +9,13 @@ public partial class CoreConfigSingboxService singboxConfig.route.final = Global.ProxyTag; var item = _config.SimpleDNSItem; - var defaultDomainResolverTag = Global.SingboxOutboundResolverTag; + var defaultDomainResolverTag = Global.SingboxDirectDNSTag; 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; + defaultDomainResolverTag = Global.SingboxLocalDNSTag; directDNSStrategy = rawDNSItem.DomainStrategy4Freedom.IsNullOrEmpty() ? Global.SingboxDomainStrategy4Out.FirstOrDefault() : rawDNSItem.DomainStrategy4Freedom; } singboxConfig.route.default_domain_resolver = new() @@ -136,13 +136,21 @@ public partial class CoreConfigSingboxService var rules = JsonUtils.Deserialize>(routing.RuleSet); foreach (var item1 in rules ?? []) { - if (item1.Enabled) + if (!item1.Enabled) { - await GenRoutingUserRule(item1, singboxConfig); - if (item1.Ip != null && item1.Ip.Count > 0) - { - ipRules.Add(item1); - } + continue; + } + + if (item1.RuleType == ERuleType.DNS) + { + continue; + } + + await GenRoutingUserRule(item1, singboxConfig); + + if (item1.Ip?.Count > 0) + { + ipRules.Add(item1); } } } @@ -242,7 +250,9 @@ public partial class CoreConfigSingboxService foreach (var it in item.Domain) { if (ParseV2Domain(it, rule1)) + { countDomain++; + } } if (countDomain > 0) { @@ -257,7 +267,9 @@ public partial class CoreConfigSingboxService foreach (var it in item.Ip) { if (ParseV2Address(it, rule2)) + { countIp++; + } } if (countIp > 0) { @@ -368,26 +380,38 @@ public partial class CoreConfigSingboxService } var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); + if (node == null - || !Global.SingboxSupportConfigType.Contains(node.ConfigType)) + || (!Global.SingboxSupportConfigType.Contains(node.ConfigType) + && !node.ConfigType.IsGroupType())) { return Global.ProxyTag; } - var tag = Global.ProxyTag + node.IndexId.ToString(); + var tag = $"{node.IndexId}-{Global.ProxyTag}"; if (singboxConfig.outbounds.Any(o => o.tag == tag) || (singboxConfig.endpoints != null && singboxConfig.endpoints.Any(e => e.tag == tag))) { return tag; } + if (node.ConfigType.IsGroupType()) + { + var ret = await GenGroupOutbound(node, singboxConfig, tag); + if (ret == 0) + { + return tag; + } + return Global.ProxyTag; + } + var server = await GenServer(node); if (server is null) { return Global.ProxyTag; } - server.tag = Global.ProxyTag + node.IndexId.ToString(); + server.tag = tag; if (server is Endpoints4Sbox endpoint) { singboxConfig.endpoints ??= new(); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs index ef611c91..7d26ca2f 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRulesetService.cs @@ -7,7 +7,9 @@ public partial class CoreConfigSingboxService static void AddRuleSets(List ruleSets, List? rule_set) { if (rule_set != null) + { ruleSets.AddRange(rule_set); + } } var geosite = "geosite"; var geoip = "geoip"; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs index b6148580..d753fb3c 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs @@ -1,6 +1,3 @@ -using System.Net; -using System.Net.NetworkInformation; - namespace ServiceLib.Services.CoreConfig; public partial class CoreConfigV2rayService(Config config) @@ -16,7 +13,7 @@ public partial class CoreConfigV2rayService(Config config) try { if (node == null - || node.Port <= 0) + || !node.IsValid()) { ret.Msg = ResUI.CheckServerSettings; return ret; @@ -30,6 +27,18 @@ public partial class CoreConfigV2rayService(Config config) ret.Msg = ResUI.InitialConfiguration; + if (node.ConfigType.IsGroupType()) + { + switch (node.ConfigType) + { + case EConfigType.PolicyGroup: + return await GenerateClientMultipleLoadConfig(node); + + case EConfigType.ProxyChain: + return await GenerateClientChainConfig(node); + } + } + var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); if (result.IsNullOrEmpty()) { @@ -71,7 +80,7 @@ public partial class CoreConfigV2rayService(Config config) } } - public async Task GenerateClientMultipleLoadConfig(List selecteds, EMultipleLoad multipleLoad) + public async Task GenerateClientMultipleLoadConfig(ProfileItem parentNode) { var ret = new RetResult(); @@ -85,8 +94,8 @@ public partial class CoreConfigV2rayService(Config config) ret.Msg = ResUI.InitialConfiguration; - string result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); - string txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty()) { ret.Msg = ResUI.FailedGetDefaultConfiguration; @@ -99,70 +108,52 @@ public partial class CoreConfigV2rayService(Config config) ret.Msg = ResUI.FailedGenDefaultConfiguration; return ret; } + v2rayConfig.outbounds.RemoveAt(0); 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) + var groupRet = await GenGroupOutbound(parentNode, v2rayConfig); + if (groupRet != 0) { ret.Msg = ResUI.FailedGenDefaultConfiguration; return ret; } - await GenOutboundsList(proxyProfiles, v2rayConfig); - //add balancers - await GenBalancer(v2rayConfig, multipleLoad); + await GenRouting(v2rayConfig); + await GenDns(null, v2rayConfig); + await GenStatistic(v2rayConfig); - var balancer = v2rayConfig.routing.balancers.First(); + var defaultBalancerTag = $"{Global.ProxyTag}{Global.BalancerTagSuffix}"; //add rule - var rules = v2rayConfig.routing.rules.Where(t => t.outboundTag == Global.ProxyTag).ToList(); - if (rules?.Count > 0) + var rules = v2rayConfig.routing.rules; + if (rules?.Count > 0 && ((v2rayConfig.routing.balancers?.Count ?? 0) > 0)) { + var balancerTagSet = v2rayConfig.routing.balancers + .Select(b => b.tag) + .ToHashSet(); + foreach (var rule in rules) { - rule.outboundTag = null; - rule.balancerTag = balancer.tag; + if (rule.outboundTag == null) + { + continue; + } + + if (balancerTagSet.Contains(rule.outboundTag)) + { + rule.balancerTag = rule.outboundTag; + rule.outboundTag = null; + continue; + } + + var outboundWithSuffix = rule.outboundTag + Global.BalancerTagSuffix; + if (balancerTagSet.Contains(outboundWithSuffix)) + { + rule.balancerTag = outboundWithSuffix; + rule.outboundTag = null; + } } } if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch) @@ -170,7 +161,7 @@ public partial class CoreConfigV2rayService(Config config) v2rayConfig.routing.rules.Add(new() { ip = ["0.0.0.0/0", "::/0"], - balancerTag = balancer.tag, + balancerTag = defaultBalancerTag, type = "field" }); } @@ -179,14 +170,71 @@ public partial class CoreConfigV2rayService(Config config) v2rayConfig.routing.rules.Add(new() { network = "tcp,udp", - balancerTag = balancer.tag, + balancerTag = defaultBalancerTag, type = "field" }); } ret.Success = true; - ret.Data = await ApplyFullConfigTemplate(v2rayConfig, true); + ret.Data = await ApplyFullConfigTemplate(v2rayConfig); + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + + public async Task GenerateClientChainConfig(ProfileItem parentNode) + { + 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; + } + v2rayConfig.outbounds.RemoveAt(0); + + await GenLog(v2rayConfig); + await GenInbounds(v2rayConfig); + + var groupRet = await GenGroupOutbound(parentNode, v2rayConfig); + if (groupRet != 0) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + await GenRouting(v2rayConfig); + await GenDns(null, v2rayConfig); + await GenStatistic(v2rayConfig); + + ret.Success = true; + + ret.Data = await ApplyFullConfigTemplate(v2rayConfig); return ret; } catch (Exception ex) @@ -255,12 +303,9 @@ public partial class CoreConfigV2rayService(Config config) continue; } var item = await AppManager.Instance.GetProfileItem(it.IndexId); - if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) + if (item is null || item.IsComplex() || !item.IsValid()) { - if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) - { - continue; - } + continue; } //find unused port @@ -289,28 +334,6 @@ public partial class CoreConfigV2rayService(Config config) 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() { @@ -321,6 +344,7 @@ public partial class CoreConfigV2rayService(Config config) inbound.tag = inbound.protocol + inbound.port.ToString(); v2rayConfig.inbounds.Add(inbound); + //outbound var outbound = JsonUtils.Deserialize(txtOutbound); await GenOutbound(item, outbound); outbound.tag = Global.ProxyTag + inbound.port.ToString(); @@ -354,7 +378,8 @@ public partial class CoreConfigV2rayService(Config config) var ret = new RetResult(); try { - if (node is not { Port: > 0 }) + if (node == null + || !node.IsValid()) { ret.Msg = ResUI.CheckServerSettings; return ret; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs index 8d2476e6..35a220c6 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs @@ -2,34 +2,89 @@ namespace ServiceLib.Services.CoreConfig; public partial class CoreConfigV2rayService { - private async Task GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad) + private async Task GenObservatory(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag) { - if (multipleLoad == EMultipleLoad.LeastPing) + // Collect all existing subject selectors from both observatories + var subjectSelectors = new List(); + subjectSelectors.AddRange(v2rayConfig.burstObservatory?.subjectSelector ?? []); + subjectSelectors.AddRange(v2rayConfig.observatory?.subjectSelector ?? []); + + // Case 1: exact match already exists -> nothing to do + if (subjectSelectors.Any(baseTagName.StartsWith)) { - var observatory = new Observatory4Ray - { - subjectSelector = [Global.ProxyTag], - probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, - probeInterval = "3m", - enableConcurrency = true, - }; - v2rayConfig.observatory = observatory; + return await Task.FromResult(0); } - else if (multipleLoad == EMultipleLoad.LeastLoad) + + // Case 2: prefix match exists -> reuse it and move to the first position + var matched = subjectSelectors.FirstOrDefault(s => s.StartsWith(baseTagName)); + if (matched is not null) { - var burstObservatory = new BurstObservatory4Ray + baseTagName = matched; + + if (v2rayConfig.burstObservatory?.subjectSelector?.Contains(baseTagName) == true) { - subjectSelector = [Global.ProxyTag], - pingConfig = new() + v2rayConfig.burstObservatory.subjectSelector.Remove(baseTagName); + v2rayConfig.burstObservatory.subjectSelector.Insert(0, baseTagName); + } + + if (v2rayConfig.observatory?.subjectSelector?.Contains(baseTagName) == true) + { + v2rayConfig.observatory.subjectSelector.Remove(baseTagName); + v2rayConfig.observatory.subjectSelector.Insert(0, baseTagName); + } + + return await Task.FromResult(0); + } + + // Case 3: need to create or insert based on multipleLoad type + if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback) + { + if (v2rayConfig.burstObservatory is null) + { + // Create new burst observatory with default ping config + v2rayConfig.burstObservatory = new BurstObservatory4Ray { - destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, - interval = "5m", - timeout = "30s", - sampling = 2, - } - }; - v2rayConfig.burstObservatory = burstObservatory; + subjectSelector = [baseTagName], + pingConfig = new() + { + destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, + interval = "5m", + timeout = "30s", + sampling = 2, + } + }; + } + else + { + v2rayConfig.burstObservatory.subjectSelector ??= new(); + v2rayConfig.burstObservatory.subjectSelector.Add(baseTagName); + } } + else if (multipleLoad is EMultipleLoad.LeastPing) + { + if (v2rayConfig.observatory is null) + { + // Create new observatory with default probe config + v2rayConfig.observatory = new Observatory4Ray + { + subjectSelector = [baseTagName], + probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, + probeInterval = "3m", + enableConcurrency = true, + }; + } + else + { + v2rayConfig.observatory.subjectSelector ??= new(); + v2rayConfig.observatory.subjectSelector.Add(baseTagName); + } + } + + return await Task.FromResult(0); + } + + private async Task GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string selector = Global.ProxyTag) + { var strategyType = multipleLoad switch { EMultipleLoad.Random => "random", @@ -38,13 +93,22 @@ public partial class CoreConfigV2rayService EMultipleLoad.LeastLoad => "leastLoad", _ => "roundRobin", }; + var balancerTag = $"{selector}{Global.BalancerTagSuffix}"; var balancer = new BalancersItem4Ray { - selector = [Global.ProxyTag], - strategy = new() { type = strategyType }, - tag = $"{Global.ProxyTag}-round", + selector = [selector], + strategy = new() + { + type = strategyType, + settings = new() + { + expected = 1, + }, + }, + tag = balancerTag, }; - v2rayConfig.routing.balancers = [balancer]; - return await Task.FromResult(0); + v2rayConfig.routing.balancers ??= new(); + v2rayConfig.routing.balancers.Add(balancer); + return await Task.FromResult(balancerTag); } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs index 5d1f7d63..1f2583ff 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs @@ -1,10 +1,8 @@ -using System.Text.Json.Nodes; - namespace ServiceLib.Services.CoreConfig; public partial class CoreConfigV2rayService { - private async Task ApplyFullConfigTemplate(V2rayConfig v2rayConfig, bool handleBalancerAndRules = false) + private async Task ApplyFullConfigTemplate(V2rayConfig v2rayConfig) { var fullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray); if (fullConfigTemplate == null || !fullConfigTemplate.Enabled || fullConfigTemplate.Config.IsNullOrEmpty()) @@ -19,7 +17,7 @@ public partial class CoreConfigV2rayService } // Handle balancer and rules modifications (for multiple load scenarios) - if (handleBalancerAndRules && v2rayConfig.routing?.balancers?.Count > 0) + if (v2rayConfig.routing?.balancers?.Count > 0) { var balancer = v2rayConfig.routing.balancers.First(); @@ -60,8 +58,36 @@ public partial class CoreConfigV2rayService } } - // Handle outbounds - append instead of override - var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray(); + if (v2rayConfig.observatory != null) + { + if (fullConfigTemplateNode["observatory"] == null) + { + fullConfigTemplateNode["observatory"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.observatory)); + } + else + { + var subjectSelector = v2rayConfig.observatory.subjectSelector; + subjectSelector.AddRange(fullConfigTemplateNode["observatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue()) ?? []); + fullConfigTemplateNode["observatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList())); + } + } + + if (v2rayConfig.burstObservatory != null) + { + if (fullConfigTemplateNode["burstObservatory"] == null) + { + fullConfigTemplateNode["burstObservatory"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.burstObservatory)); + } + else + { + var subjectSelector = v2rayConfig.burstObservatory.subjectSelector; + subjectSelector.AddRange(fullConfigTemplateNode["burstObservatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue()) ?? []); + fullConfigTemplateNode["burstObservatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList())); + } + } + + var customOutboundsNode = new JsonArray(); + foreach (var outbound in v2rayConfig.outbounds) { if (outbound.protocol.ToLower() is "blackhole" or "dns" or "freedom") @@ -71,14 +97,30 @@ public partial class CoreConfigV2rayService 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))) + else if ((!fullConfigTemplate.ProxyDetour.IsNullOrEmpty()) + && ((outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() ?? true) == true)) { - outbound.streamSettings ??= new StreamSettings4Ray(); - outbound.streamSettings.sockopt ??= new Sockopt4Ray(); - outbound.streamSettings.sockopt.dialerProxy = fullConfigTemplate.ProxyDetour; + var outboundAddress = outbound.settings?.servers?.FirstOrDefault()?.address + ?? outbound.settings?.vnext?.FirstOrDefault()?.address + ?? string.Empty; + if (!Utils.IsPrivateNetwork(outboundAddress)) + { + outbound.streamSettings ??= new StreamSettings4Ray(); + outbound.streamSettings.sockopt ??= new Sockopt4Ray(); + outbound.streamSettings.sockopt.dialerProxy = fullConfigTemplate.ProxyDetour; + } } customOutboundsNode.Add(JsonUtils.DeepCopy(outbound)); } + + if (fullConfigTemplateNode["outbounds"] is JsonArray templateOutbounds) + { + foreach (var outbound in templateOutbounds) + { + customOutboundsNode.Add(outbound?.DeepClone()); + } + } + 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 index 744286d4..c24b1eb2 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs @@ -1,7 +1,3 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - namespace ServiceLib.Services.CoreConfig; public partial class CoreConfigV2rayService @@ -83,9 +79,23 @@ public partial class CoreConfigV2rayService static object CreateDnsServer(string dnsAddress, List domains, List? expectedIPs = null) { + var (domain, scheme, port, path) = Utils.ParseUrl(dnsAddress); + var domainFinal = dnsAddress; + int? portFinal = null; + if (scheme.IsNullOrEmpty() || scheme.StartsWith("udp", StringComparison.OrdinalIgnoreCase)) + { + domainFinal = domain; + portFinal = port > 0 ? port : null; + } + else if (scheme.StartsWith("tcp", StringComparison.OrdinalIgnoreCase)) + { + domainFinal = scheme + "://" + domain; + portFinal = port > 0 ? port : null; + } var dnsServer = new DnsServer4Ray { - address = dnsAddress, + address = domainFinal, + port = portFinal, skipFallback = true, domains = domains.Count > 0 ? domains : null, expectedIPs = expectedIPs?.Count > 0 ? expectedIPs : null @@ -107,6 +117,35 @@ public partial class CoreConfigV2rayService var expectedIPs = new List(); var regionNames = new HashSet(); + var bootstrapDNSAddress = ParseDnsAddresses(simpleDNSItem?.BootstrapDNS, Global.DomainPureIPDNSAddress.FirstOrDefault()); + var dnsServerDomains = new List(); + + foreach (var dns in directDNSAddress) + { + var (domain, _, _, _) = Utils.ParseUrl(dns); + if (domain == "localhost") + { + continue; + } + if (Utils.IsDomain(domain)) + { + dnsServerDomains.Add($"full:{domain}"); + } + } + foreach (var dns in remoteDNSAddress) + { + var (domain, _, _, _) = Utils.ParseUrl(dns); + if (domain == "localhost") + { + continue; + } + if (Utils.IsDomain(domain)) + { + dnsServerDomains.Add($"full:{domain}"); + } + } + dnsServerDomains = dnsServerDomains.Distinct().ToList(); + if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs)) { expectedIPs = simpleDNSItem.DirectExpectedIPs @@ -142,10 +181,18 @@ public partial class CoreConfigV2rayService continue; } + if (item.RuleType == ERuleType.Routing) + { + continue; + } + foreach (var domain in item.Domain) { if (domain.StartsWith('#')) + { continue; + } + var normalizedDomain = domain.Replace(Global.RoutingRuleComma, ","); if (item.OutboundTag == Global.DirectTag) @@ -216,6 +263,10 @@ public partial class CoreConfigV2rayService AddDnsServers(remoteDNSAddress, proxyGeositeList); AddDnsServers(directDNSAddress, directGeositeList); AddDnsServers(directDNSAddress, expectedDomainList, expectedIPs); + if (dnsServerDomains.Count > 0) + { + AddDnsServers(bootstrapDNSAddress, dnsServerDomains); + } var useDirectDns = rules?.LastOrDefault() is { } lastRule && lastRule.OutboundTag == Global.DirectTag @@ -299,8 +350,8 @@ public partial class CoreConfigV2rayService if (obj is null) { List servers = []; - string[] arrDNS = normalDNS.Split(','); - foreach (string str in arrDNS) + var arrDNS = normalDNS.Split(','); + foreach (var str in arrDNS) { servers.Add(str); } @@ -320,7 +371,10 @@ public partial class CoreConfigV2rayService foreach (var host in systemHosts) { if (normalHost1[host.Key] != null) + { continue; + } + normalHost1[host.Key] = host.Value; } } @@ -349,7 +403,7 @@ public partial class CoreConfigV2rayService return 0; } - private async Task GenDnsDomainsCompatible(ProfileItem? node, JsonNode dns, DNSItem? dNSItem) + private async Task GenDnsDomainsCompatible(ProfileItem? node, JsonNode dns, DNSItem? dnsItem) { if (node == null) { @@ -388,7 +442,7 @@ public partial class CoreConfigV2rayService { var dnsServer = new DnsServer4Ray() { - address = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dNSItem?.DomainDNSAddress, + address = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress, skipFallback = true, domains = domainList }; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs index 7753c21e..2cbdfe88 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs @@ -48,7 +48,7 @@ public partial class CoreConfigV2rayService private Inbounds4Ray GetInbound(InItem inItem, EInboundProtocol protocol, bool bSocks) { - string result = EmbedUtils.GetEmbedText(Global.V2raySampleInbound); + var result = EmbedUtils.GetEmbedText(Global.V2raySampleInbound); if (result.IsNullOrEmpty()) { return new(); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index 11e8a8fa..351c7bf0 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -277,6 +277,23 @@ public partial class CoreConfigV2rayService { tlsSettings.serverName = Utils.String2List(host)?.First(); } + var certs = CertPemManager.ParsePemChain(node.Cert); + if (certs.Count > 0) + { + var certsettings = new List(); + foreach (var cert in certs) + { + var certPerLine = cert.Split("\n").ToList(); + certsettings.Add(new CertificateSettings4Ray + { + certificate = certPerLine, + usage = "verify", + }); + } + tlsSettings.certificates = certsettings; + tlsSettings.disableSystemRoot = true; + tlsSettings.allowInsecure = false; + } streamSettings.tlsSettings = tlsSettings; } @@ -453,16 +470,16 @@ public partial class CoreConfigV2rayService }; //request Host - string request = EmbedUtils.GetEmbedText(Global.V2raySampleHttpRequestFileName); - string[] arrHost = host.Split(','); - string host2 = string.Join(",".AppendQuotes(), arrHost); + var request = EmbedUtils.GetEmbedText(Global.V2raySampleHttpRequestFileName); + var arrHost = host.Split(','); + var host2 = string.Join(",".AppendQuotes(), arrHost); request = request.Replace("$requestHost$", $"{host2.AppendQuotes()}"); request = request.Replace("$requestUserAgent$", $"{useragent.AppendQuotes()}"); //Path - string pathHttp = @"/"; + var pathHttp = @"/"; if (path.IsNotEmpty()) { - string[] arrPath = path.Split(','); + var arrPath = path.Split(','); pathHttp = string.Join(",".AppendQuotes(), arrPath); } request = request.Replace("$requestPath$", $"{pathHttp.AppendQuotes()}"); @@ -480,6 +497,60 @@ public partial class CoreConfigV2rayService return 0; } + private async Task GenGroupOutbound(ProfileItem node, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false) + { + try + { + if (!node.ConfigType.IsGroupType()) + { + return -1; + } + var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId); + if (hasCycle) + { + return -1; + } + + var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + if (childProfiles.Count <= 0) + { + return -1; + } + switch (node.ConfigType) + { + case EConfigType.PolicyGroup: + if (ignoreOriginChain) + { + await GenOutboundsList(childProfiles, v2rayConfig, baseTagName); + } + else + { + await GenOutboundsListWithChain(childProfiles, v2rayConfig, baseTagName); + } + break; + + case EConfigType.ProxyChain: + await GenChainOutboundsList(childProfiles, v2rayConfig, baseTagName); + break; + + default: + break; + } + + //add balancers + if (node.ConfigType == EConfigType.PolicyGroup) + { + await GenObservatory(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName); + await GenBalancer(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + private async Task GenMoreOutbounds(ProfileItem node, V2rayConfig v2rayConfig) { //fragment proxy @@ -552,7 +623,7 @@ public partial class CoreConfigV2rayService return 0; } - private async Task GenOutboundsList(List nodes, V2rayConfig v2rayConfig) + private async Task GenOutboundsListWithChain(List nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag) { try { @@ -569,14 +640,33 @@ public partial class CoreConfigV2rayService // 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 + var prevIndex = 0; // Index for prev outbounds // Process nodes - int index = 0; + var index = 0; foreach (var node in nodes) { index++; + if (node.ConfigType.IsGroupType()) + { + var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + if (childProfiles.Count <= 0) + { + continue; + } + var childBaseTagName = $"{baseTagName}-{index}"; + var ret = node.ConfigType switch + { + EConfigType.PolicyGroup => + await GenOutboundsListWithChain(childProfiles, v2rayConfig, childBaseTagName), + EConfigType.ProxyChain => + await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName), + _ => throw new NotImplementedException() + }; + continue; + } + // Handle proxy chain string? prevTag = null; var currentOutbound = JsonUtils.Deserialize(txtOutbound); @@ -590,7 +680,7 @@ public partial class CoreConfigV2rayService // current proxy await GenOutbound(node, currentOutbound); - currentOutbound.tag = $"{Global.ProxyTag}-{index}"; + currentOutbound.tag = $"{baseTagName}-{index}"; if (!node.Subid.IsNullOrEmpty()) { @@ -606,7 +696,7 @@ public partial class CoreConfigV2rayService { var prevOutbound = JsonUtils.Deserialize(txtOutbound); await GenOutbound(prevNode, prevOutbound); - prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}"; + prevTag = $"prev-{baseTagName}-{++prevIndex}"; prevOutbound.tag = prevTag; prevOutbounds.Add(prevOutbound); } @@ -628,9 +718,17 @@ public partial class CoreConfigV2rayService } // Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds - resultOutbounds.AddRange(prevOutbounds); - resultOutbounds.AddRange(v2rayConfig.outbounds); - v2rayConfig.outbounds = resultOutbounds; + if (baseTagName == Global.ProxyTag) + { + resultOutbounds.AddRange(prevOutbounds); + resultOutbounds.AddRange(v2rayConfig.outbounds); + v2rayConfig.outbounds = resultOutbounds; + } + else + { + v2rayConfig.outbounds.AddRange(prevOutbounds); + v2rayConfig.outbounds.AddRange(resultOutbounds); + } } catch (Exception ex) { @@ -692,4 +790,113 @@ public partial class CoreConfigV2rayService } return null; } + + private async Task GenOutboundsList(List nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag) + { + var resultOutbounds = new List(); + for (var i = 0; i < nodes.Count; i++) + { + var node = nodes[i]; + if (node == null) + { + continue; + } + + if (node.ConfigType.IsGroupType()) + { + var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + if (childProfiles.Count <= 0) + { + continue; + } + var childBaseTagName = $"{baseTagName}-{i + 1}"; + var ret = node.ConfigType switch + { + EConfigType.PolicyGroup => + await GenOutboundsListWithChain(childProfiles, v2rayConfig, childBaseTagName), + EConfigType.ProxyChain => + await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName), + _ => throw new NotImplementedException() + }; + continue; + } + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + if (txtOutbound.IsNullOrEmpty()) + { + break; + } + var outbound = JsonUtils.Deserialize(txtOutbound); + var result = await GenOutbound(node, outbound); + if (result != 0) + { + break; + } + outbound.tag = baseTagName + (i + 1).ToString(); + resultOutbounds.Add(outbound); + } + if (baseTagName == Global.ProxyTag) + { + resultOutbounds.AddRange(v2rayConfig.outbounds); + v2rayConfig.outbounds = resultOutbounds; + } + else + { + v2rayConfig.outbounds.AddRange(resultOutbounds); + } + return await Task.FromResult(0); + } + + private async Task GenChainOutboundsList(List nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag) + { + // Based on actual network flow instead of data packets + var nodesReverse = nodes.AsEnumerable().Reverse().ToList(); + var resultOutbounds = new List(); + for (var i = 0; i < nodesReverse.Count; i++) + { + var node = nodesReverse[i]; + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + if (txtOutbound.IsNullOrEmpty()) + { + break; + } + var outbound = JsonUtils.Deserialize(txtOutbound); + var result = await GenOutbound(node, outbound); + + if (result != 0) + { + break; + } + + if (i == 0) + { + outbound.tag = baseTagName; + } + else + { + // avoid v2ray observe + outbound.tag = "chain-" + baseTagName + i.ToString(); + } + + if (i != nodesReverse.Count - 1) + { + outbound.streamSettings.sockopt = new() + { + dialerProxy = "chain-" + baseTagName + (i + 1).ToString() + }; + } + + resultOutbounds.Add(outbound); + } + if (baseTagName == Global.ProxyTag) + { + resultOutbounds.AddRange(v2rayConfig.outbounds); + v2rayConfig.outbounds = resultOutbounds; + } + else + { + v2rayConfig.outbounds.AddRange(resultOutbounds); + } + + return await Task.FromResult(0); + } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs index d39ea833..83df2ab2 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs @@ -20,11 +20,18 @@ public partial class CoreConfigV2rayService var rules = JsonUtils.Deserialize>(routing.RuleSet); foreach (var item in rules) { - if (item.Enabled) + if (!item.Enabled) { - var item2 = JsonUtils.Deserialize(JsonUtils.Serialize(item)); - await GenRoutingUserRule(item2, v2rayConfig); + continue; } + + if (item.RuleType == ERuleType.DNS) + { + continue; + } + + var item2 = JsonUtils.Deserialize(JsonUtils.Serialize(item)); + await GenRoutingUserRule(item2, v2rayConfig); } } } @@ -125,18 +132,30 @@ public partial class CoreConfigV2rayService } var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); + if (node == null - || !Global.XraySupportConfigType.Contains(node.ConfigType)) + || (!Global.XraySupportConfigType.Contains(node.ConfigType) + && !node.ConfigType.IsGroupType())) { return Global.ProxyTag; } - var tag = Global.ProxyTag + node.IndexId.ToString(); + var tag = $"{node.IndexId}-{Global.ProxyTag}"; if (v2rayConfig.outbounds.Any(p => p.tag == tag)) { return tag; } + if (node.ConfigType.IsGroupType()) + { + var ret = await GenGroupOutbound(node, v2rayConfig, tag); + if (ret == 0) + { + return tag; + } + return Global.ProxyTag; + } + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); var outbound = JsonUtils.Deserialize(txtOutbound); await GenOutbound(node, outbound); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs index 1269a11f..b2ec37b4 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayStatisticService.cs @@ -6,7 +6,7 @@ public partial class CoreConfigV2rayService { if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed) { - string tag = EInboundProtocol.api.ToString(); + var tag = EInboundProtocol.api.ToString(); Metrics4Ray apiObj = new(); Policy4Ray policyObj = new(); SystemPolicy4Ray policySystemSetting = new(); diff --git a/v2rayN/ServiceLib/Services/DownloadService.cs b/v2rayN/ServiceLib/Services/DownloadService.cs index 99170646..fe0da3d6 100644 --- a/v2rayN/ServiceLib/Services/DownloadService.cs +++ b/v2rayN/ServiceLib/Services/DownloadService.cs @@ -1,6 +1,4 @@ -using System.Net; using System.Net.Http.Headers; -using System.Net.Sockets; namespace ServiceLib.Services; @@ -19,8 +17,6 @@ public class DownloadService { try { - SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); - var progress = new Progress(); progress.ProgressChanged += (sender, value) => updateFunc?.Invoke(false, $"{value}"); @@ -44,7 +40,6 @@ public class DownloadService { try { - SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); UpdateCompleted?.Invoke(this, new RetResult(false, $"{ResUI.Downloading} {url}")); var progress = new Progress(); @@ -71,13 +66,12 @@ public class DownloadService 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 client = new HttpClient(webRequestHandler); var response = await client.GetAsync(url); if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null) @@ -141,7 +135,6 @@ public class DownloadService { try { - SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); var webProxy = await GetWebProxy(blProxy); var client = new HttpClient(new SocketsHttpHandler() { @@ -163,7 +156,7 @@ public class DownloadService } using var cts = new CancellationTokenSource(); - var result = await HttpClientHelper.Instance.GetAsync(client, url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token); + var result = await client.GetStringAsync(url, cts.Token).WaitAsync(TimeSpan.FromSeconds(timeout), cts.Token); return result; } catch (Exception ex) @@ -186,8 +179,6 @@ public class DownloadService { try { - SetSecurityProtocol(AppManager.Instance.Config.GuiItem.EnableSecurityProtocolTls13); - var webProxy = await GetWebProxy(blProxy); if (userAgent.IsNullOrEmpty()) @@ -238,17 +229,4 @@ public class DownloadService 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/ProcessService.cs b/v2rayN/ServiceLib/Services/ProcessService.cs new file mode 100644 index 00000000..0f7161e3 --- /dev/null +++ b/v2rayN/ServiceLib/Services/ProcessService.cs @@ -0,0 +1,180 @@ +namespace ServiceLib.Services; + +public class ProcessService : IDisposable +{ + private readonly Process _process; + private readonly Func? _updateFunc; + private bool _isDisposed; + + public int Id => _process.Id; + public IntPtr Handle => _process.Handle; + public bool HasExited => _process.HasExited; + + public ProcessService( + string fileName, + string arguments, + string workingDirectory, + bool displayLog, + bool redirectInput, + Dictionary? environmentVars, + Func? updateFunc) + { + _updateFunc = updateFunc; + + _process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + WorkingDirectory = workingDirectory, + UseShellExecute = false, + RedirectStandardInput = redirectInput, + RedirectStandardOutput = displayLog, + RedirectStandardError = displayLog, + CreateNoWindow = true, + StandardOutputEncoding = displayLog ? Encoding.UTF8 : null, + StandardErrorEncoding = displayLog ? Encoding.UTF8 : null, + }, + EnableRaisingEvents = true + }; + + if (environmentVars != null) + { + foreach (var kv in environmentVars) + { + _process.StartInfo.Environment[kv.Key] = kv.Value; + } + } + + if (displayLog) + { + RegisterEventHandlers(); + } + } + + public async Task StartAsync(string pwd = null) + { + _process.Start(); + + if (_process.StartInfo.RedirectStandardOutput) + { + _process.BeginOutputReadLine(); + _process.BeginErrorReadLine(); + } + + if (_process.StartInfo.RedirectStandardInput) + { + await Task.Delay(10); + await _process.StandardInput.WriteLineAsync(pwd); + } + } + + public async Task StopAsync() + { + if (_process.HasExited) + { + return; + } + + try + { + if (_process.StartInfo.RedirectStandardOutput) + { + try + { + _process.CancelOutputRead(); + } + catch { } + try + { + _process.CancelErrorRead(); + } + catch { } + } + + try + { + if (Utils.IsNonWindows()) + { + _process.Kill(true); + } + } + catch { } + + try + { + _process.Kill(); + } + catch { } + + await Task.Delay(100); + } + catch (Exception ex) + { + await _updateFunc?.Invoke(true, ex.Message); + } + } + + private void RegisterEventHandlers() + { + void dataHandler(object sender, DataReceivedEventArgs e) + { + if (e.Data.IsNotEmpty()) + { + _ = _updateFunc?.Invoke(false, e.Data + Environment.NewLine); + } + } + + _process.OutputDataReceived += dataHandler; + _process.ErrorDataReceived += dataHandler; + + _process.Exited += (s, e) => + { + try + { + _process.OutputDataReceived -= dataHandler; + _process.ErrorDataReceived -= dataHandler; + } + catch + { + } + }; + } + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + try + { + if (!_process.HasExited) + { + try + { + _process.CancelOutputRead(); + } + catch { } + try + { + _process.CancelErrorRead(); + } + catch { } + + _process.Kill(); + } + + _process.Dispose(); + } + catch (Exception ex) + { + _updateFunc?.Invoke(true, ex.Message); + } + + _isDisposed = true; + GC.SuppressFinalize(this); + } +} diff --git a/v2rayN/ServiceLib/Services/SpeedtestService.cs b/v2rayN/ServiceLib/Services/SpeedtestService.cs index 5266f4d0..600ba4d1 100644 --- a/v2rayN/ServiceLib/Services/SpeedtestService.cs +++ b/v2rayN/ServiceLib/Services/SpeedtestService.cs @@ -1,8 +1,3 @@ -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Net; -using System.Net.Sockets; - namespace ServiceLib.Services; public class SpeedtestService(Config config, Func updateFunc) @@ -26,7 +21,7 @@ public class SpeedtestService(Config config, Func updateF { if (_lstExitLoop.Count > 0) { - UpdateFunc("", ResUI.SpeedtestingStop); + _ = UpdateFunc("", ResUI.SpeedtestingStop); _lstExitLoop.Clear(); } @@ -64,7 +59,7 @@ public class SpeedtestService(Config config, Func updateF var lstSelected = new List(); foreach (var it in selecteds) { - if (it.ConfigType == EConfigType.Custom) + if (it.ConfigType.IsComplexType()) { continue; } @@ -116,10 +111,6 @@ public class SpeedtestService(Config config, Func updateF List tasks = []; foreach (var it in selecteds) { - if (it.ConfigType == EConfigType.Custom) - { - continue; - } tasks.Add(Task.Run(async () => { try @@ -182,11 +173,11 @@ public class SpeedtestService(Config config, Func updateF private async Task RunRealPingAsync(List selecteds, string exitLoopKey) { - var pid = -1; + ProcessService processService = null; try { - pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds); - if (pid < 0) + processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds); + if (processService is null) { return false; } @@ -199,10 +190,6 @@ public class SpeedtestService(Config config, Func updateF { continue; } - if (it.ConfigType == EConfigType.Custom) - { - continue; - } tasks.Add(Task.Run(async () => { await DoRealPing(it); @@ -216,9 +203,9 @@ public class SpeedtestService(Config config, Func updateF } finally { - if (pid > 0) + if (processService != null) { - await ProcUtils.ProcessKill(pid); + await processService?.StopAsync(); } } return true; @@ -236,19 +223,15 @@ public class SpeedtestService(Config config, Func updateF await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); continue; } - if (it.ConfigType == EConfigType.Custom) - { - continue; - } await concurrencySemaphore.WaitAsync(); tasks.Add(Task.Run(async () => { - var pid = -1; + ProcessService processService = null; try { - pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(it); - if (pid < 0) + processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(it); + if (processService is null) { await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore); } @@ -275,9 +258,9 @@ public class SpeedtestService(Config config, Func updateF } finally { - if (pid > 0) + if (processService != null) { - await ProcUtils.ProcessKill(pid); + await processService?.StopAsync(); } concurrencySemaphore.Release(); } @@ -289,7 +272,7 @@ public class SpeedtestService(Config config, Func updateF 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); + var responseTime = await ConnectionHandler.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); await UpdateFunc(it.IndexId, responseTime.ToString()); @@ -351,7 +334,7 @@ public class SpeedtestService(Config config, Func updateF { 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(); + var lst2 = lstSelected.Where(t => Global.SingboxOnlyConfigType.Contains(t.ConfigType)).ToList(); for (var num = 0; num < (int)Math.Ceiling(lst1.Count * 1.0 / pageSize); num++) { diff --git a/v2rayN/ServiceLib/Services/Statistics/StatisticsSingboxService.cs b/v2rayN/ServiceLib/Services/Statistics/StatisticsSingboxService.cs index d3de6a18..2b0f53f1 100644 --- a/v2rayN/ServiceLib/Services/Statistics/StatisticsSingboxService.cs +++ b/v2rayN/ServiceLib/Services/Statistics/StatisticsSingboxService.cs @@ -1,5 +1,4 @@ using System.Net.WebSockets; -using System.Text; namespace ServiceLib.Services.Statistics; diff --git a/v2rayN/ServiceLib/Services/UpdateService.cs b/v2rayN/ServiceLib/Services/UpdateService.cs index 429c0a62..fc636c60 100644 --- a/v2rayN/ServiceLib/Services/UpdateService.cs +++ b/v2rayN/ServiceLib/Services/UpdateService.cs @@ -1,17 +1,14 @@ -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; - namespace ServiceLib.Services; -public class UpdateService +public class UpdateService(Config config, Func updateFunc) { - private Func? _updateFunc; + private readonly Config? _config = config; + private readonly Func? _updateFunc = updateFunc; private readonly int _timeout = 30; private static readonly string _tag = "UpdateService"; - public async Task CheckUpdateGuiN(Config config, Func updateFunc, bool preRelease) + public async Task CheckUpdateGuiN(bool preRelease) { - _updateFunc = updateFunc; var url = string.Empty; var fileName = string.Empty; @@ -50,9 +47,8 @@ public class UpdateService } } - public async Task CheckUpdateCore(ECoreType type, Config config, Func updateFunc, bool preRelease) + public async Task CheckUpdateCore(ECoreType type, bool preRelease) { - _updateFunc = updateFunc; var url = string.Empty; var fileName = string.Empty; @@ -104,11 +100,11 @@ public class UpdateService } } - public async Task UpdateGeoFileAll(Config config, Func updateFunc) + public async Task UpdateGeoFileAll() { - await UpdateGeoFiles(config, updateFunc); - await UpdateOtherFiles(config, updateFunc); - await UpdateSrsFileAll(config, updateFunc); + await UpdateGeoFiles(); + await UpdateOtherFiles(); + await UpdateSrsFileAll(); await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo")); } @@ -170,7 +166,7 @@ public class UpdateService try { var coreInfo = CoreInfoManager.Instance.GetCoreInfo(type); - string filePath = string.Empty; + var filePath = string.Empty; foreach (var name in coreInfo.CoreExes) { var vName = Utils.GetBinPath(Utils.GetExeName(name), coreInfo.CoreType.ToString()); @@ -183,14 +179,14 @@ public class UpdateService if (!File.Exists(filePath)) { - string msg = string.Format(ResUI.NotFoundCore, @"", "", ""); + var 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; + var version = string.Empty; switch (type) { case ECoreType.v2fly: @@ -317,7 +313,7 @@ public class UpdateService _ => null, }; } - else if (Utils.IsOSX()) + else if (Utils.IsMacOS()) { return RuntimeInformation.ProcessArchitecture switch { @@ -333,13 +329,11 @@ public class UpdateService #region Geo private - private async Task UpdateGeoFiles(Config config, Func updateFunc) + private async Task UpdateGeoFiles() { - _updateFunc = updateFunc; - - var geoUrl = string.IsNullOrEmpty(config?.ConstItem.GeoSourceUrl) + var geoUrl = string.IsNullOrEmpty(_config?.ConstItem.GeoSourceUrl) ? Global.GeoUrl - : config.ConstItem.GeoSourceUrl; + : _config.ConstItem.GeoSourceUrl; List files = ["geosite", "geoip"]; foreach (var geoName in files) @@ -348,33 +342,29 @@ public class UpdateService var targetPath = Utils.GetBinPath($"{fileName}"); var url = string.Format(geoUrl, geoName); - await DownloadGeoFile(url, fileName, targetPath, updateFunc); + await DownloadGeoFile(url, fileName, targetPath); } } - private async Task UpdateOtherFiles(Config config, Func updateFunc) + private async Task UpdateOtherFiles() { //If it is not in China area, no update is required - if (config.ConstItem.GeoSourceUrl.IsNotEmpty()) + 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); + await DownloadGeoFile(url, fileName, targetPath); } } - private async Task UpdateSrsFileAll(Config config, Func updateFunc) + private async Task UpdateSrsFileAll() { - _updateFunc = updateFunc; - var geoipFiles = new List(); var geoSiteFiles = new List(); @@ -417,29 +407,29 @@ public class UpdateService } foreach (var item in geoipFiles.Distinct()) { - await UpdateSrsFile("geoip", item, config, updateFunc); + await UpdateSrsFile("geoip", item); } foreach (var item in geoSiteFiles.Distinct()) { - await UpdateSrsFile("geosite", item, config, updateFunc); + await UpdateSrsFile("geosite", item); } } - private async Task UpdateSrsFile(string type, string srsName, Config config, Func updateFunc) + private async Task UpdateSrsFile(string type, string srsName) { - var srsUrl = string.IsNullOrEmpty(config.ConstItem.SrsSourceUrl) + var srsUrl = string.IsNullOrEmpty(_config.ConstItem.SrsSourceUrl) ? Global.SingboxRulesetUrl - : config.ConstItem.SrsSourceUrl; + : _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); + await DownloadGeoFile(url, fileName, targetPath); } - private async Task DownloadGeoFile(string url, string fileName, string targetPath, Func updateFunc) + private async Task DownloadGeoFile(string url, string fileName, string targetPath) { var tmpFileName = Utils.GetTempPath(Utils.GetGuid()); diff --git a/v2rayN/ServiceLib/Services/WindowsJobService.cs b/v2rayN/ServiceLib/Services/WindowsJobService.cs new file mode 100644 index 00000000..ffd4a36a --- /dev/null +++ b/v2rayN/ServiceLib/Services/WindowsJobService.cs @@ -0,0 +1,171 @@ +namespace ServiceLib.Services; + +/// +/// http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net +/// +public sealed class WindowsJobService : IDisposable +{ + private nint handle = nint.Zero; + + public WindowsJobService() + { + handle = CreateJobObject(nint.Zero, null); + var extendedInfoPtr = nint.Zero; + var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION + { + LimitFlags = 0x2000 + }; + + var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + BasicLimitInformation = info + }; + + try + { + var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); + extendedInfoPtr = Marshal.AllocHGlobal(length); + Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); + + if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, + (uint)length)) + { + throw new Exception(string.Format("Unable to set information. Error: {0}", + Marshal.GetLastWin32Error())); + } + } + finally + { + if (extendedInfoPtr != nint.Zero) + { + Marshal.FreeHGlobal(extendedInfoPtr); + } + } + } + + public bool AddProcess(nint processHandle) + { + var succ = AssignProcessToJobObject(handle, processHandle); + + if (!succ) + { + Logging.SaveLog("Failed to call AssignProcessToJobObject! GetLastError=" + Marshal.GetLastWin32Error()); + } + + return succ; + } + + public bool AddProcess(int processId) + { + return AddProcess(Process.GetProcessById(processId).Handle); + } + + #region IDisposable + + private bool disposed; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposed) + { + return; + } + disposed = true; + + if (disposing) + { + // no managed objects to free + } + + if (handle != nint.Zero) + { + CloseHandle(handle); + handle = nint.Zero; + } + } + + ~WindowsJobService() + { + Dispose(false); + } + + #endregion IDisposable + + #region Interop + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + private static extern nint CreateJobObject(nint a, string? lpName); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetInformationJobObject(nint hJob, JobObjectInfoType infoType, nint lpJobObjectInfo, uint cbJobObjectInfoLength); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool AssignProcessToJobObject(nint job, nint process); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(nint hObject); + + #endregion Interop +} + +[StructLayout(LayoutKind.Sequential)] +internal struct IO_COUNTERS +{ + public ulong ReadOperationCount; + public ulong WriteOperationCount; + public ulong OtherOperationCount; + public ulong ReadTransferCount; + public ulong WriteTransferCount; + public ulong OtherTransferCount; +} + +[StructLayout(LayoutKind.Sequential)] +internal struct JOBOBJECT_BASIC_LIMIT_INFORMATION +{ + public long PerProcessUserTimeLimit; + public long PerJobUserTimeLimit; + public uint LimitFlags; + public nuint MinimumWorkingSetSize; + public nuint MaximumWorkingSetSize; + public uint ActiveProcessLimit; + public nuint Affinity; + public uint PriorityClass; + public uint SchedulingClass; +} + +[StructLayout(LayoutKind.Sequential)] +public struct SECURITY_ATTRIBUTES +{ + public uint nLength; + public nint lpSecurityDescriptor; + public int bInheritHandle; +} + +[StructLayout(LayoutKind.Sequential)] +internal struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION +{ + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public nuint ProcessMemoryLimit; + public nuint JobMemoryLimit; + public nuint PeakProcessMemoryUsed; + public nuint PeakJobMemoryUsed; +} + +public enum JobObjectInfoType +{ + AssociateCompletionPortInformation = 7, + BasicLimitInformation = 2, + BasicUIRestrictions = 4, + EndOfJobTimeInformation = 6, + ExtendedLimitInformation = 9, + SecurityLimitInformation = 5, + GroupInformation = 11 +} diff --git a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs new file mode 100644 index 00000000..5b0778a5 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs @@ -0,0 +1,249 @@ +namespace ServiceLib.ViewModels; + +public class AddGroupServerViewModel : MyReactiveObject +{ + [Reactive] + public ProfileItem SelectedSource { get; set; } + + [Reactive] + public ProfileItem SelectedChild { get; set; } + + [Reactive] + public IList SelectedChildren { get; set; } + + [Reactive] + public string? CoreType { get; set; } + + [Reactive] + public string? PolicyGroupType { get; set; } + + [Reactive] + public SubItem? SelectedSubItem { get; set; } + + [Reactive] + public string? Filter { get; set; } + + public IObservableCollection SubItems { get; } = new ObservableCollectionExtended(); + + public IObservableCollection ChildItemsObs { get; } = new ObservableCollectionExtended(); + + //public ReactiveCommand AddCmd { get; } + public ReactiveCommand RemoveCmd { get; } + + public ReactiveCommand MoveTopCmd { get; } + public ReactiveCommand MoveUpCmd { get; } + public ReactiveCommand MoveDownCmd { get; } + public ReactiveCommand MoveBottomCmd { get; } + + public ReactiveCommand SaveCmd { get; } + + public AddGroupServerViewModel(ProfileItem profileItem, Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + var canEditRemove = this.WhenAnyValue( + x => x.SelectedChild, + SelectedChild => SelectedChild != null && !SelectedChild.Remarks.IsNullOrEmpty()); + + RemoveCmd = ReactiveCommand.CreateFromTask(async () => + { + await ChildRemoveAsync(); + }, canEditRemove); + 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); + SaveCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveServerAsync(); + }); + + SelectedSource = profileItem.IndexId.IsNullOrEmpty() ? profileItem : JsonUtils.DeepCopy(profileItem); + CoreType = (SelectedSource?.CoreType ?? ECoreType.Xray).ToString(); + + _ = Init(); + } + + public async Task Init() + { + ProfileGroupItemManager.Instance.TryGet(SelectedSource.IndexId, out var profileGroup); + PolicyGroupType = (profileGroup?.MultipleLoad ?? EMultipleLoad.LeastPing) switch + { + EMultipleLoad.LeastPing => ResUI.TbLeastPing, + EMultipleLoad.Fallback => ResUI.TbFallback, + EMultipleLoad.Random => ResUI.TbRandom, + EMultipleLoad.RoundRobin => ResUI.TbRoundRobin, + EMultipleLoad.LeastLoad => ResUI.TbLeastLoad, + _ => ResUI.TbLeastPing, + }; + + var subs = await AppManager.Instance.SubItems(); + subs.Add(new SubItem()); + SubItems.AddRange(subs); + SelectedSubItem = SubItems.Where(s => s.Id == profileGroup?.SubChildItems).FirstOrDefault(); + Filter = profileGroup?.Filter; + + var childItemMulti = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource?.IndexId); + if (childItemMulti != null) + { + var childIndexIds = Utils.String2List(childItemMulti.ChildItems) ?? []; + foreach (var item in childIndexIds) + { + var child = await AppManager.Instance.GetProfileItem(item); + if (child == null) + { + continue; + } + ChildItemsObs.Add(child); + } + } + } + + public async Task ChildRemoveAsync() + { + if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return; + } + foreach (var it in SelectedChildren ?? [SelectedChild]) + { + if (it != null) + { + ChildItemsObs.Remove(it); + } + } + await Task.CompletedTask; + } + + public async Task MoveServer(EMove eMove) + { + if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return; + } + var index = ChildItemsObs.IndexOf(SelectedChild); + if (index < 0) + { + return; + } + var selectedChild = JsonUtils.DeepCopy(SelectedChild); + switch (eMove) + { + case EMove.Top: + if (index == 0) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Insert(0, selectedChild); + break; + + case EMove.Up: + if (index == 0) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Insert(index - 1, selectedChild); + break; + + case EMove.Down: + if (index == ChildItemsObs.Count - 1) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Insert(index + 1, selectedChild); + break; + + case EMove.Bottom: + if (index == ChildItemsObs.Count - 1) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Add(selectedChild); + break; + + default: + break; + } + await Task.CompletedTask; + } + + private async Task SaveServerAsync() + { + var remarks = SelectedSource.Remarks; + if (remarks.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); + return; + } + if (ChildItemsObs.Count == 0 && SelectedSubItem?.Id.IsNullOrEmpty() == true) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer); + return; + } + SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? ECoreType.Xray : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); + if (SelectedSource.CoreType is not (ECoreType.Xray or ECoreType.sing_box) || + SelectedSource.ConfigType is not (EConfigType.ProxyChain or EConfigType.PolicyGroup)) + { + return; + } + var childIndexIds = new List(); + foreach (var item in ChildItemsObs) + { + if (item.IndexId.IsNullOrEmpty()) + { + continue; + } + childIndexIds.Add(item.IndexId); + } + var profileGroup = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource.IndexId); + profileGroup.ChildItems = Utils.List2String(childIndexIds); + profileGroup.MultipleLoad = PolicyGroupType switch + { + var s when s == ResUI.TbLeastPing => EMultipleLoad.LeastPing, + var s when s == ResUI.TbFallback => EMultipleLoad.Fallback, + var s when s == ResUI.TbRandom => EMultipleLoad.Random, + var s when s == ResUI.TbRoundRobin => EMultipleLoad.RoundRobin, + var s when s == ResUI.TbLeastLoad => EMultipleLoad.LeastLoad, + _ => EMultipleLoad.LeastPing, + }; + + profileGroup.SubChildItems = SelectedSubItem?.Id; + profileGroup.Filter = Filter; + + var hasCycle = ProfileGroupItemManager.HasCycle(profileGroup.IndexId); + if (hasCycle) + { + NoticeManager.Instance.Enqueue(string.Format(ResUI.GroupSelfReference, remarks)); + return; + } + + if (await ConfigHandler.AddGroupServerCommon(_config, SelectedSource, profileGroup, true) == 0) + { + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + _updateView?.Invoke(EViewAction.CloseWindow, null); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } +} diff --git a/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs index a94ecf74..cbc97642 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs @@ -1,7 +1,3 @@ -using System.Reactive; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.ViewModels; public class AddServer2ViewModel : MyReactiveObject diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index cd2399bc..804287c5 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -1,7 +1,3 @@ -using System.Reactive; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.ViewModels; public class AddServerViewModel : MyReactiveObject @@ -12,6 +8,14 @@ public class AddServerViewModel : MyReactiveObject [Reactive] public string? CoreType { get; set; } + [Reactive] + public string Cert { get; set; } + + [Reactive] + public string CertTip { get; set; } + + public ReactiveCommand FetchCertCmd { get; } + public ReactiveCommand FetchCertChainCmd { get; } public ReactiveCommand SaveCmd { get; } public AddServerViewModel(ProfileItem profileItem, Func>? updateView) @@ -19,11 +23,22 @@ public class AddServerViewModel : MyReactiveObject _config = AppManager.Instance.Config; _updateView = updateView; + FetchCertCmd = ReactiveCommand.CreateFromTask(async () => + { + await FetchCert(); + }); + FetchCertChainCmd = ReactiveCommand.CreateFromTask(async () => + { + await FetchCertChain(); + }); SaveCmd = ReactiveCommand.CreateFromTask(async () => { await SaveServerAsync(); }); + this.WhenAnyValue(x => x.Cert) + .Subscribe(_ => UpdateCertTip()); + if (profileItem.IndexId.IsNullOrEmpty()) { profileItem.Network = Global.DefaultNetwork; @@ -37,6 +52,7 @@ public class AddServerViewModel : MyReactiveObject SelectedSource = JsonUtils.DeepCopy(profileItem); } CoreType = SelectedSource?.CoreType?.ToString(); + Cert = SelectedSource?.Cert?.ToString() ?? string.Empty; } private async Task SaveServerAsync() @@ -81,6 +97,7 @@ public class AddServerViewModel : MyReactiveObject } } SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); + SelectedSource.Cert = Cert.IsNullOrEmpty() ? null : Cert; if (await ConfigHandler.AddServer(_config, SelectedSource) == 0) { @@ -92,4 +109,72 @@ public class AddServerViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.OperationFailed); } } + + private void UpdateCertTip(string? errorMessage = null) + { + CertTip = errorMessage.IsNullOrEmpty() + ? (Cert.IsNullOrEmpty() ? ResUI.CertNotSet : ResUI.CertSet) + : errorMessage; + } + + private async Task FetchCert() + { + if (SelectedSource.StreamSecurity != Global.StreamSecurity) + { + return; + } + var domain = SelectedSource.Address; + var serverName = SelectedSource.Sni; + if (serverName.IsNullOrEmpty()) + { + serverName = SelectedSource.RequestHost; + } + if (serverName.IsNullOrEmpty()) + { + serverName = SelectedSource.Address; + } + if (!Utils.IsDomain(serverName)) + { + UpdateCertTip(ResUI.ServerNameMustBeValidDomain); + return; + } + if (SelectedSource.Port > 0) + { + domain += $":{SelectedSource.Port}"; + } + string certError; + (Cert, certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName); + UpdateCertTip(certError); + } + + private async Task FetchCertChain() + { + if (SelectedSource.StreamSecurity != Global.StreamSecurity) + { + return; + } + var domain = SelectedSource.Address; + var serverName = SelectedSource.Sni; + if (serverName.IsNullOrEmpty()) + { + serverName = SelectedSource.RequestHost; + } + if (serverName.IsNullOrEmpty()) + { + serverName = SelectedSource.Address; + } + if (!Utils.IsDomain(serverName)) + { + UpdateCertTip(ResUI.ServerNameMustBeValidDomain); + return; + } + if (SelectedSource.Port > 0) + { + domain += $":{SelectedSource.Port}"; + } + string certError; + (var certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName); + Cert = CertPemManager.ConcatenatePemChain(certs); + UpdateCertTip(certError); + } } diff --git a/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs b/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs index 5a96a04d..dbe3b24d 100644 --- a/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs @@ -1,7 +1,3 @@ -using System.Reactive; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.ViewModels; public class BackupAndRestoreViewModel : MyReactiveObject @@ -123,7 +119,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject return; } //check - var lstFiles = FileManager.GetFilesFromZip(fileName); + var lstFiles = FileUtils.GetFilesFromZip(fileName); if (lstFiles is null || !lstFiles.Any(t => t.Contains(_guiConfigs))) { DisplayOperationMsg(ResUI.LocalRestoreInvalidZipTips); @@ -139,7 +135,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject await SQLiteHelper.Instance.DisposeDbConnectionAsync(); var toPath = Utils.GetConfigPath(); - FileManager.ZipExtractToFile(fileName, toPath, ""); + FileUtils.ZipExtractToFile(fileName, toPath, ""); if (Utils.IsWindows()) { @@ -171,8 +167,8 @@ public class BackupAndRestoreViewModel : MyReactiveObject 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); + FileUtils.CopyDirectory(configDir, configDirTemp, false, true, ""); + var ret = FileUtils.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 index 4c9c0550..7bcb36ea 100644 --- a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs @@ -1,13 +1,3 @@ -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 @@ -38,7 +28,7 @@ public class CheckUpdateViewModel : MyReactiveObject this.WhenAnyValue( x => x.EnableCheckPreReleaseUpdate, y => y == true) - .Subscribe(c => { _config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate; }); + .Subscribe(c => _config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate); RefreshCheckUpdateItems(); } @@ -158,11 +148,8 @@ public class CheckUpdateViewModel : MyReactiveObject UpdatedPlusPlus(_geo, ""); } } - await (new UpdateService()).UpdateGeoFileAll(_config, _updateUI) - .ContinueWith(t => - { - UpdatedPlusPlus(_geo, ""); - }); + await new UpdateService(_config, _updateUI).UpdateGeoFileAll() + .ContinueWith(t => UpdatedPlusPlus(_geo, "")); } private async Task CheckUpdateN(bool preRelease) @@ -176,11 +163,8 @@ public class CheckUpdateViewModel : MyReactiveObject UpdatedPlusPlus(_v2rayN, msg); } } - await (new UpdateService()).CheckUpdateGuiN(_config, _updateUI, preRelease) - .ContinueWith(t => - { - UpdatedPlusPlus(_v2rayN, ""); - }); + await new UpdateService(_config, _updateUI).CheckUpdateGuiN(preRelease) + .ContinueWith(t => UpdatedPlusPlus(_v2rayN, "")); } private async Task CheckUpdateCore(CheckUpdateModel model, bool preRelease) @@ -196,11 +180,8 @@ public class CheckUpdateViewModel : MyReactiveObject } } var type = (ECoreType)Enum.Parse(typeof(ECoreType), model.CoreType); - await (new UpdateService()).CheckUpdateCore(type, _config, _updateUI, preRelease) - .ContinueWith(t => - { - UpdatedPlusPlus(model.CoreType, ""); - }); + await new UpdateService(_config, _updateUI).CheckUpdateCore(type, preRelease) + .ContinueWith(t => UpdatedPlusPlus(model.CoreType, "")); } private async Task UpdateFinished() @@ -228,17 +209,18 @@ public class CheckUpdateViewModel : MyReactiveObject _ = UpdateFinishedResult(blReload); return Disposable.Empty; }); + await Task.CompletedTask; } public async Task UpdateFinishedResult(bool blReload) { if (blReload) { - Locator.Current.GetService()?.Reload(); + AppEvents.ReloadRequested.Publish(); } else { - Locator.Current.GetService()?.CloseCore(); + await CoreManager.Instance.CoreStop(); } } @@ -289,29 +271,29 @@ public class CheckUpdateViewModel : MyReactiveObject if (fileName.Contains(".tar.gz")) { - FileManager.DecompressTarFile(fileName, toPath); + FileUtils.DecompressTarFile(fileName, toPath); var dir = new DirectoryInfo(toPath); if (dir.Exists) { foreach (var subDir in dir.GetDirectories()) { - FileManager.CopyDirectory(subDir.FullName, toPath, false, true); + FileUtils.CopyDirectory(subDir.FullName, toPath, false, true); subDir.Delete(true); } } } else if (fileName.Contains(".gz")) { - FileManager.DecompressFile(fileName, toPath, item.CoreType); + FileUtils.DecompressFile(fileName, toPath, item.CoreType); } else { - FileManager.ZipExtractToFile(fileName, toPath, "geo"); + FileUtils.ZipExtractToFile(fileName, toPath, "geo"); } if (Utils.IsNonWindows()) { - var filesList = (new DirectoryInfo(toPath)).GetFiles().Select(u => u.FullName).ToList(); + 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())); @@ -340,6 +322,7 @@ public class CheckUpdateViewModel : MyReactiveObject _ = UpdateViewResult(model); return Disposable.Empty; }); + await Task.CompletedTask; } public async Task UpdateViewResult(CheckUpdateModel model) @@ -349,6 +332,7 @@ public class CheckUpdateViewModel : MyReactiveObject { return; } - found.Remarks = model.Remarks; + found.Remarks = model.Remarks; + await Task.CompletedTask; } } diff --git a/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs index d45b8e7d..dba8190f 100644 --- a/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs @@ -1,11 +1,3 @@ -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 @@ -104,6 +96,7 @@ public class ClashConnectionsViewModel : MyReactiveObject } ConnectionItems.AddRange(lstModel); + await Task.CompletedTask; } public async Task ClashConnectionClose(bool all) diff --git a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs index 343d49df..d49193ad 100644 --- a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs @@ -1,11 +1,4 @@ -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; @@ -69,6 +62,8 @@ public class ClashProxiesViewModel : MyReactiveObject SortingSelected = _config.ClashUIItem.ProxiesSorting; RuleModeSelected = (int)_config.ClashUIItem.RuleMode; + #region WhenAnyValue && ReactiveCommand + this.WhenAnyValue( x => x.SelectedGroup, y => y != null && y.Name.IsNotEmpty()) @@ -89,6 +84,17 @@ public class ClashProxiesViewModel : MyReactiveObject y => y == true) .Subscribe(c => { _config.ClashUIItem.ProxiesAutoRefresh = AutoRefresh; }); + #endregion WhenAnyValue && ReactiveCommand + + #region AppEvents + + AppEvents.ProxiesReloadRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await ProxiesReload()); + + #endregion AppEvents + _ = Init(); } @@ -205,7 +211,7 @@ public class ClashProxiesViewModel : MyReactiveObject } //from api - foreach (KeyValuePair kv in _proxies) + foreach (var kv in _proxies) { if (!Global.allowSelectType.Contains(kv.Value.type.ToLower())) { @@ -239,6 +245,7 @@ public class ClashProxiesViewModel : MyReactiveObject { SelectedGroup = new(); } + await Task.CompletedTask; } private void RefreshProxyDetails(bool c) @@ -313,7 +320,7 @@ public class ClashProxiesViewModel : MyReactiveObject //from providers if (_providers != null) { - foreach (KeyValuePair kv in _providers) + foreach (var kv in _providers) { if (Global.proxyVehicleType.Contains(kv.Value.vehicleType.ToLower())) { @@ -385,6 +392,7 @@ public class ClashProxiesViewModel : MyReactiveObject _ = ProxiesDelayTestResult(model); return Disposable.Empty; }); + await Task.CompletedTask; }); await Task.CompletedTask; } @@ -413,6 +421,7 @@ public class ClashProxiesViewModel : MyReactiveObject detail.Delay = _delayTimeout; detail.DelayName = string.Empty; } + await Task.CompletedTask; } #endregion proxy function diff --git a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs index 9ca2d407..d53b960d 100644 --- a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs @@ -1,7 +1,3 @@ -using System.Reactive; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.ViewModels; public class DNSSettingViewModel : MyReactiveObject @@ -12,8 +8,7 @@ public class DNSSettingViewModel : MyReactiveObject [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? BootstrapDNS { get; set; } [Reactive] public string? RayStrategy4Freedom { get; set; } [Reactive] public string? SingboxStrategy4Direct { get; set; } [Reactive] public string? SingboxStrategy4Proxy { get; set; } @@ -32,6 +27,8 @@ public class DNSSettingViewModel : MyReactiveObject [Reactive] public bool RayCustomDNSEnableCompatible { get; set; } [Reactive] public bool SBCustomDNSEnableCompatible { get; set; } + [ObservableAsProperty] public bool IsSimpleDNSEnabled { get; } + public ReactiveCommand SaveCmd { get; } public ReactiveCommand ImportDefConfig4V2rayCompatibleCmd { get; } public ReactiveCommand ImportDefConfig4SingboxCompatibleCmd { get; } @@ -55,6 +52,10 @@ public class DNSSettingViewModel : MyReactiveObject await Task.CompletedTask; }); + this.WhenAnyValue(x => x.RayCustomDNSEnableCompatible, x => x.SBCustomDNSEnableCompatible) + .Select(x => !(x.Item1 && x.Item2)) + .ToPropertyEx(this, x => x.IsSimpleDNSEnabled); + _ = Init(); } @@ -68,9 +69,8 @@ public class DNSSettingViewModel : MyReactiveObject BlockBindingQuery = item.BlockBindingQuery; DirectDNS = item.DirectDNS; RemoteDNS = item.RemoteDNS; + BootstrapDNS = item.BootstrapDNS; RayStrategy4Freedom = item.RayStrategy4Freedom; - SingboxOutboundsResolveDNS = item.SingboxOutboundsResolveDNS; - SingboxFinalResolveDNS = item.SingboxFinalResolveDNS; SingboxStrategy4Direct = item.SingboxStrategy4Direct; SingboxStrategy4Proxy = item.SingboxStrategy4Proxy; Hosts = item.Hosts; @@ -99,9 +99,8 @@ public class DNSSettingViewModel : MyReactiveObject _config.SimpleDNSItem.BlockBindingQuery = BlockBindingQuery; _config.SimpleDNSItem.DirectDNS = DirectDNS; _config.SimpleDNSItem.RemoteDNS = RemoteDNS; + _config.SimpleDNSItem.BootstrapDNS = BootstrapDNS; _config.SimpleDNSItem.RayStrategy4Freedom = RayStrategy4Freedom; - _config.SimpleDNSItem.SingboxOutboundsResolveDNS = SingboxOutboundsResolveDNS; - _config.SimpleDNSItem.SingboxFinalResolveDNS = SingboxFinalResolveDNS; _config.SimpleDNSItem.SingboxStrategy4Direct = SingboxStrategy4Direct; _config.SimpleDNSItem.SingboxStrategy4Proxy = SingboxStrategy4Proxy; _config.SimpleDNSItem.Hosts = Hosts; diff --git a/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs b/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs index 3619ddef..3a50b52e 100644 --- a/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs @@ -1,7 +1,3 @@ -using System.Reactive; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.ViewModels; public class FullConfigTemplateViewModel : MyReactiveObject @@ -70,10 +66,14 @@ public class FullConfigTemplateViewModel : MyReactiveObject private async Task SaveSettingAsync() { if (!await SaveXrayConfigAsync()) + { return; + } if (!await SaveSingboxConfigAsync()) + { return; + } NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); _ = _updateView?.Invoke(EViewAction.CloseWindow, null); diff --git a/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs index 0acb8726..581007f1 100644 --- a/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs @@ -1,6 +1,3 @@ -using System.Reactive; -using ReactiveUI; - namespace ServiceLib.ViewModels; public class GlobalHotkeySettingViewModel : MyReactiveObject diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index 4e9fc9fc..c3bb0fe3 100644 --- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -1,8 +1,4 @@ -using System.Reactive; using System.Reactive.Concurrency; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using Splat; namespace ServiceLib.ViewModels; @@ -23,6 +19,8 @@ public class MainWindowViewModel : MyReactiveObject public ReactiveCommand AddWireguardServerCmd { get; } public ReactiveCommand AddAnytlsServerCmd { get; } public ReactiveCommand AddCustomServerCmd { get; } + public ReactiveCommand AddPolicyGroupServerCmd { get; } + public ReactiveCommand AddProxyChainServerCmd { get; } public ReactiveCommand AddServerViaClipboardCmd { get; } public ReactiveCommand AddServerViaScanCmd { get; } public ReactiveCommand AddServerViaImageCmd { get; } @@ -64,9 +62,9 @@ public class MainWindowViewModel : MyReactiveObject [Reactive] public int TabMainSelectedIndex { get; set; } - #endregion Menu + [Reactive] public bool BlIsWindows { get; set; } - private bool _hasNextReloadJob = false; + #endregion Menu #region Init @@ -74,53 +72,62 @@ public class MainWindowViewModel : MyReactiveObject { _config = AppManager.Instance.Config; _updateView = updateView; + BlIsWindows = Utils.IsWindows(); #region WhenAnyValue && ReactiveCommand //servers AddVmessServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.VMess); + await AddServerAsync(EConfigType.VMess); }); AddVlessServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.VLESS); + await AddServerAsync(EConfigType.VLESS); }); AddShadowsocksServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.Shadowsocks); + await AddServerAsync(EConfigType.Shadowsocks); }); AddSocksServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.SOCKS); + await AddServerAsync(EConfigType.SOCKS); }); AddHttpServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.HTTP); + await AddServerAsync(EConfigType.HTTP); }); AddTrojanServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.Trojan); + await AddServerAsync(EConfigType.Trojan); }); AddHysteria2ServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.Hysteria2); + await AddServerAsync(EConfigType.Hysteria2); }); AddTuicServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.TUIC); + await AddServerAsync(EConfigType.TUIC); }); AddWireguardServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.WireGuard); + await AddServerAsync(EConfigType.WireGuard); }); AddAnytlsServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.Anytls); + await AddServerAsync(EConfigType.Anytls); }); AddCustomServerCmd = ReactiveCommand.CreateFromTask(async () => { - await AddServerAsync(true, EConfigType.Custom); + await AddServerAsync(EConfigType.Custom); + }); + AddPolicyGroupServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(EConfigType.PolicyGroup); + }); + AddProxyChainServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(EConfigType.ProxyChain); }); AddServerViaClipboardCmd = ReactiveCommand.CreateFromTask(async () => { @@ -184,7 +191,7 @@ public class MainWindowViewModel : MyReactiveObject }); RebootAsAdminCmd = ReactiveCommand.CreateFromTask(async () => { - await RebootAsAdmin(); + await AppManager.Instance.RebootAsAdmin(); }); ClearServerStatisticsCmd = ReactiveCommand.CreateFromTask(async () => { @@ -217,6 +224,30 @@ public class MainWindowViewModel : MyReactiveObject #endregion WhenAnyValue && ReactiveCommand + #region AppEvents + + AppEvents.ReloadRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await Reload()); + + AppEvents.AddServerViaScanRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await AddServerViaScanAsync()); + + AppEvents.AddServerViaClipboardRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await AddServerViaClipboardAsync(null)); + + AppEvents.SubscriptionsUpdateRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy)); + + #endregion AppEvents + _ = Init(); } @@ -224,10 +255,11 @@ public class MainWindowViewModel : MyReactiveObject { _config.UiItem.ShowInTaskbar = true; - await ConfigHandler.InitBuiltinRouting(_config); + //await ConfigHandler.InitBuiltinRouting(_config); await ConfigHandler.InitBuiltinDNS(_config); await ConfigHandler.InitBuiltinFullConfigTemplate(_config); await ProfileExManager.Instance.Init(); + await ProfileGroupItemManager.Instance.Init(); await CoreManager.Instance.Init(_config, UpdateHandler); TaskManager.Instance.RegUpdateTask(_config, UpdateTaskHandler); @@ -237,10 +269,7 @@ public class MainWindowViewModel : MyReactiveObject } await RefreshServers(); - BlReloadEnabled = true; await Reload(); - await AutoHideStartup(); - Locator.Current.GetService()?.RefreshRoutingsMenu(); } #endregion Init @@ -254,6 +283,7 @@ public class MainWindowViewModel : MyReactiveObject { NoticeManager.Instance.Enqueue(msg); } + await Task.CompletedTask; } private async Task UpdateTaskHandler(bool success, string msg) @@ -269,7 +299,7 @@ public class MainWindowViewModel : MyReactiveObject } if (_config.UiItem.EnableAutoAdjustMainLvColWidth) { - AppEvents.AdjustMainLvColWidthRequested.OnNext(Unit.Default); + AppEvents.AdjustMainLvColWidthRequested.Publish(); } } } @@ -280,12 +310,8 @@ public class MainWindowViewModel : MyReactiveObject { return; } - AppEvents.DispatcherStatisticsRequested.OnNext(update); - } - - public void ShowHideWindow(bool? blShow) - { - _updateView?.Invoke(EViewAction.ShowHideWindow, blShow); + AppEvents.DispatcherStatisticsRequested.Publish(update); + await Task.CompletedTask; } #endregion Actions @@ -294,21 +320,21 @@ public class MainWindowViewModel : MyReactiveObject private async Task RefreshServers() { - AppEvents.ProfilesRefreshRequested.OnNext(Unit.Default); + AppEvents.ProfilesRefreshRequested.Publish(); await Task.Delay(200); } private void RefreshSubscriptions() { - Locator.Current.GetService()?.RefreshSubscriptions(); + AppEvents.SubscriptionsRefreshRequested.Publish(); } #endregion Servers && Groups #region Add Servers - public async Task AddServerAsync(bool blNew, EConfigType eConfigType) + public async Task AddServerAsync(EConfigType eConfigType) { ProfileItem item = new() { @@ -322,6 +348,10 @@ public class MainWindowViewModel : MyReactiveObject { ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); } + else if (eConfigType.IsGroupType()) + { + ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); + } else { ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); @@ -433,7 +463,7 @@ public class MainWindowViewModel : MyReactiveObject var ret = await _updateView?.Invoke(EViewAction.OptionSettingWindow, null); if (ret == true) { - Locator.Current.GetService()?.InboundDisplayStatus(); + AppEvents.InboundDisplayRequested.Publish(); await Reload(); } } @@ -444,7 +474,7 @@ public class MainWindowViewModel : MyReactiveObject if (ret == true) { await ConfigHandler.InitBuiltinRouting(_config); - Locator.Current.GetService()?.RefreshRoutingsMenu(); + AppEvents.RoutingsMenuRefreshRequested.Publish(); await Reload(); } } @@ -467,12 +497,6 @@ public class MainWindowViewModel : MyReactiveObject } } - public async Task RebootAsAdmin() - { - ProcUtils.RebootAsAdmin(); - await AppManager.Instance.AppExitAsync(true); - } - private async Task ClearServerStatistics() { await StatisticsManager.Instance.ClearAllServerStatistics(); @@ -490,7 +514,7 @@ public class MainWindowViewModel : MyReactiveObject { ProcUtils.ProcessStart("xdg-open", path); } - else if (Utils.IsOSX()) + else if (Utils.IsMacOS()) { ProcUtils.ProcessStart("open", path); } @@ -501,48 +525,74 @@ public class MainWindowViewModel : MyReactiveObject #region core job + private bool _hasNextReloadJob = false; + private readonly SemaphoreSlim _reloadSemaphore = new(1, 1); + public async Task Reload() { //If there are unfinished reload job, marked with next job. - if (!BlReloadEnabled) + if (!await _reloadSemaphore.WaitAsync(0)) { _hasNextReloadJob = true; return; } - BlReloadEnabled = false; - - await Task.Run(async () => + try { - await LoadCore(); - await SysProxyHandler.UpdateSysProxy(_config, false); - await Task.Delay(1000); - }); - Locator.Current.GetService()?.TestServerAvailability(); + SetReloadEnabled(false); - RxApp.MainThreadScheduler.Schedule(() => _ = ReloadResult()); + var msgs = await ActionPrecheckManager.Instance.Check(_config.IndexId); + if (msgs.Count > 0) + { + foreach (var msg in msgs) + { + NoticeManager.Instance.SendMessage(msg); + } + NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true)); + return; + } - BlReloadEnabled = true; - if (_hasNextReloadJob) + await Task.Run(async () => + { + await LoadCore(); + await SysProxyHandler.UpdateSysProxy(_config, false); + await Task.Delay(1000); + }); + AppEvents.TestServerRequested.Publish(); + + var showClashUI = _config.IsRunningCore(ECoreType.sing_box); + if (showClashUI) + { + AppEvents.ProxiesReloadRequested.Publish(); + } + + ReloadResult(showClashUI); + } + finally { - _hasNextReloadJob = false; - await Reload(); + SetReloadEnabled(true); + _reloadSemaphore.Release(); + //If there is a next reload job, execute it. + if (_hasNextReloadJob) + { + _hasNextReloadJob = false; + await Reload(); + } } } - public async Task ReloadResult() + private void ReloadResult(bool showClashUI) { - // BlReloadEnabled = true; - //Locator.Current.GetService()?.ChangeSystemProxyAsync(_config.systemProxyItem.sysProxyType, false); - ShowClashUI = _config.IsRunningCore(ECoreType.sing_box); - if (ShowClashUI) + RxApp.MainThreadScheduler.Schedule(() => { - Locator.Current.GetService()?.ProxiesReload(); - } - else - { - TabMainSelectedIndex = 0; - } + ShowClashUI = showClashUI; + TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0; + }); + } + + private void SetReloadEnabled(bool enabled) + { + RxApp.MainThreadScheduler.Schedule(() => BlReloadEnabled = enabled); } private async Task LoadCore() @@ -551,21 +601,6 @@ public class MainWindowViewModel : MyReactiveObject 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 @@ -574,10 +609,10 @@ public class MainWindowViewModel : MyReactiveObject { await ConfigHandler.ApplyRegionalPreset(_config, type); await ConfigHandler.InitRouting(_config); - Locator.Current.GetService()?.RefreshRoutingsMenu(); + AppEvents.RoutingsMenuRefreshRequested.Publish(); await ConfigHandler.SaveConfig(_config); - await new UpdateService().UpdateGeoFileAll(_config, UpdateTaskHandler); + await new UpdateService(_config, UpdateTaskHandler).UpdateGeoFileAll(); await Reload(); } diff --git a/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs b/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs index 8fc62dfe..08a77d33 100644 --- a/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs @@ -1,17 +1,11 @@ -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; + private volatile bool _lastMsgFilterNotAvailable; + private int _showLock = 0; // 0 = unlocked, 1 = locked + public int NumMaxMsg { get; } = 500; [Reactive] public string MsgFilter { get; set; } @@ -33,46 +27,52 @@ public class MsgViewModel : MyReactiveObject this.WhenAnyValue( x => x.AutoRefresh, y => y == true) - .Subscribe(c => { _config.MsgUIItem.AutoRefresh = AutoRefresh; }); + .Subscribe(c => _config.MsgUIItem.AutoRefresh = AutoRefresh); AppEvents.SendMsgViewRequested .AsObservable() //.ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(async content => await AppendQueueMsg(content)); + .Subscribe(content => _ = AppendQueueMsg(content)); } private async Task AppendQueueMsg(string msg) { - //if (msg == Global.CommandClearMsg) - //{ - // ClearMsg(); - // return; - //} if (AutoRefresh == false) { return; } - _ = EnqueueQueueMsg(msg); - if (_blLockShow) - { - return; - } + EnqueueQueueMsg(msg); + if (!_config.UiItem.ShowInTaskbar) { return; } - _blLockShow = true; + if (Interlocked.CompareExchange(ref _showLock, 1, 0) != 0) + { + return; + } - await Task.Delay(500); - var txt = string.Join("", _queueMsg.ToArray()); - await _updateView?.Invoke(EViewAction.DispatcherShowMsg, txt); + try + { + await Task.Delay(500).ConfigureAwait(false); - _blLockShow = false; + var sb = new StringBuilder(); + while (_queueMsg.TryDequeue(out var line)) + { + sb.Append(line); + } + + await _updateView?.Invoke(EViewAction.DispatcherShowMsg, sb.ToString()); + } + finally + { + Interlocked.Exchange(ref _showLock, 0); + } } - private async Task EnqueueQueueMsg(string msg) + private void EnqueueQueueMsg(string msg) { //filter msg if (MsgFilter.IsNotEmpty() && !_lastMsgFilterNotAvailable) @@ -91,26 +91,17 @@ public class MsgViewModel : MyReactiveObject } } - //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(); - } + //public void ClearMsg() + //{ + // _queueMsg.Clear(); + //} private void DoMsgFilter() { diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs index 7f446cf2..73ec2681 100644 --- a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs @@ -1,7 +1,3 @@ -using System.Reactive; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.ViewModels; public class OptionSettingViewModel : MyReactiveObject @@ -52,9 +48,9 @@ public class OptionSettingViewModel : MyReactiveObject [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 MacOSShowInDock { get; set; } [Reactive] public bool EnableDragDropSort { get; set; } [Reactive] public bool DoubleClick2Activate { get; set; } [Reactive] public int AutoUpdateInterval { get; set; } @@ -74,11 +70,22 @@ public class OptionSettingViewModel : MyReactiveObject #endregion UI + #region UI visibility + + [Reactive] public bool BlIsWindows { get; set; } + [Reactive] public bool BlIsLinux { get; set; } + [Reactive] public bool BlIsIsMacOS { get; set; } + [Reactive] public bool BlIsNonWindows { get; set; } + + #endregion UI visibility + #region System proxy [Reactive] public bool notProxyLocalAddress { get; set; } [Reactive] public string systemProxyAdvancedProtocol { get; set; } [Reactive] public string systemProxyExceptions { get; set; } + [Reactive] public string CustomSystemProxyPacPath { get; set; } + [Reactive] public string CustomSystemProxyScriptPath { get; set; } #endregion System proxy @@ -111,6 +118,10 @@ public class OptionSettingViewModel : MyReactiveObject { _config = AppManager.Instance.Config; _updateView = updateView; + BlIsWindows = Utils.IsWindows(); + BlIsLinux = Utils.IsLinux(); + BlIsIsMacOS = Utils.IsMacOS(); + BlIsNonWindows = Utils.IsNonWindows(); SaveCmd = ReactiveCommand.CreateFromTask(async () => { @@ -170,9 +181,9 @@ public class OptionSettingViewModel : MyReactiveObject KeepOlderDedupl = _config.GuiItem.KeepOlderDedupl; EnableAutoAdjustMainLvColWidth = _config.UiItem.EnableAutoAdjustMainLvColWidth; EnableUpdateSubOnlyRemarksExist = _config.UiItem.EnableUpdateSubOnlyRemarksExist; - EnableSecurityProtocolTls13 = _config.GuiItem.EnableSecurityProtocolTls13; AutoHideStartup = _config.UiItem.AutoHideStartup; Hide2TrayWhenClose = _config.UiItem.Hide2TrayWhenClose; + MacOSShowInDock = _config.UiItem.MacOSShowInDock; EnableDragDropSort = _config.UiItem.EnableDragDropSort; DoubleClick2Activate = _config.UiItem.DoubleClick2Activate; AutoUpdateInterval = _config.GuiItem.AutoUpdateInterval; @@ -197,6 +208,8 @@ public class OptionSettingViewModel : MyReactiveObject notProxyLocalAddress = _config.SystemProxyItem.NotProxyLocalAddress; systemProxyAdvancedProtocol = _config.SystemProxyItem.SystemProxyAdvancedProtocol; systemProxyExceptions = _config.SystemProxyItem.SystemProxyExceptions; + CustomSystemProxyPacPath = _config.SystemProxyItem.CustomSystemProxyPacPath; + CustomSystemProxyScriptPath = _config.SystemProxyItem.CustomSystemProxyScriptPath; #endregion System proxy @@ -279,12 +292,12 @@ public class OptionSettingViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort); return; } - var needReboot = (EnableStatistics != _config.GuiItem.EnableStatistics + 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); + || MainGirdOrientation != (int)_config.UiItem.MainGirdOrientation; //if (Utile.IsNullOrEmpty(Kcpmtu.ToString()) || !Utile.IsNumeric(Kcpmtu.ToString()) // || Utile.IsNullOrEmpty(Kcptti.ToString()) || !Utile.IsNumeric(Kcptti.ToString()) @@ -330,9 +343,9 @@ public class OptionSettingViewModel : MyReactiveObject _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.UiItem.MacOSShowInDock = MacOSShowInDock; _config.GuiItem.AutoUpdateInterval = AutoUpdateInterval; _config.UiItem.EnableDragDropSort = EnableDragDropSort; _config.UiItem.DoubleClick2Activate = DoubleClick2Activate; @@ -354,6 +367,8 @@ public class OptionSettingViewModel : MyReactiveObject _config.SystemProxyItem.SystemProxyExceptions = systemProxyExceptions; _config.SystemProxyItem.NotProxyLocalAddress = notProxyLocalAddress; _config.SystemProxyItem.SystemProxyAdvancedProtocol = systemProxyAdvancedProtocol; + _config.SystemProxyItem.CustomSystemProxyPacPath = CustomSystemProxyPacPath; + _config.SystemProxyItem.CustomSystemProxyScriptPath = CustomSystemProxyScriptPath; //tun mode _config.TunModeItem.AutoRoute = TunAutoRoute; @@ -382,7 +397,7 @@ public class OptionSettingViewModel : MyReactiveObject private async Task SaveCoreType() { - for (int k = 1; k <= _config.CoreTypeItem.Count; k++) + for (var k = 1; k <= _config.CoreTypeItem.Count; k++) { var item = _config.CoreTypeItem[k - 1]; var type = string.Empty; diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs index 8742755e..7301882c 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs @@ -1,9 +1,3 @@ -using System.Reactive.Linq; -using DynamicData; -using DynamicData.Binding; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.ViewModels; public class ProfilesSelectViewModel : MyReactiveObject diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 4c0fd2b9..a6bb2567 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -1,13 +1,3 @@ -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 @@ -53,11 +43,13 @@ public class ProfilesViewModel : MyReactiveObject 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; } + public ReactiveCommand GenGroupMultipleServerXrayRandomCmd { get; } + public ReactiveCommand GenGroupMultipleServerXrayRoundRobinCmd { get; } + public ReactiveCommand GenGroupMultipleServerXrayLeastPingCmd { get; } + public ReactiveCommand GenGroupMultipleServerXrayLeastLoadCmd { get; } + public ReactiveCommand GenGroupMultipleServerXrayFallbackCmd { get; } + public ReactiveCommand GenGroupMultipleServerSingBoxLeastPingCmd { get; } + public ReactiveCommand GenGroupMultipleServerSingBoxFallbackCmd { get; } //servers move public ReactiveCommand MoveTopCmd { get; } @@ -74,6 +66,7 @@ public class ProfilesViewModel : MyReactiveObject public ReactiveCommand SpeedServerCmd { get; } public ReactiveCommand SortServerResultCmd { get; } public ReactiveCommand RemoveInvalidServerResultCmd { get; } + public ReactiveCommand FastRealPingCmd { get; } //servers export public ReactiveCommand Export2ClientConfigCmd { get; } @@ -117,7 +110,7 @@ public class ProfilesViewModel : MyReactiveObject //servers delete EditServerCmd = ReactiveCommand.CreateFromTask(async () => { - await EditServerAsync(EConfigType.Custom); + await EditServerAsync(); }, canEditRemove); RemoveServerCmd = ReactiveCommand.CreateFromTask(async () => { @@ -139,25 +132,33 @@ public class ProfilesViewModel : MyReactiveObject { await ShareServerAsync(); }, canEditRemove); - SetDefaultMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () => + GenGroupMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () => { - await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.Random); + await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Random); }, canEditRemove); - SetDefaultMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () => + GenGroupMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () => { - await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin); + await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin); }, canEditRemove); - SetDefaultMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () => + GenGroupMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () => { - await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing); + await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing); }, canEditRemove); - SetDefaultMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () => + GenGroupMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () => { - await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad); + await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad); }, canEditRemove); - SetDefaultMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () => + GenGroupMultipleServerXrayFallbackCmd = ReactiveCommand.CreateFromTask(async () => { - await SetDefaultMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing); + await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Fallback); + }, canEditRemove); + GenGroupMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () => + { + await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing); + }, canEditRemove); + GenGroupMultipleServerSingBoxFallbackCmd = ReactiveCommand.CreateFromTask(async () => + { + await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.Fallback); }, canEditRemove); //servers move @@ -179,6 +180,10 @@ public class ProfilesViewModel : MyReactiveObject }, canEditRemove); //servers ping + FastRealPingCmd = ReactiveCommand.CreateFromTask(async () => + { + await ServerSpeedtest(ESpeedActionType.FastRealping); + }); MixedTestServerCmd = ReactiveCommand.CreateFromTask(async () => { await ServerSpeedtest(ESpeedActionType.Mixedtest); @@ -240,11 +245,21 @@ public class ProfilesViewModel : MyReactiveObject .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(async _ => await RefreshServersBiz()); + AppEvents.SubscriptionsRefreshRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await RefreshSubscriptions()); + AppEvents.DispatcherStatisticsRequested .AsObservable() .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(async result => await UpdateStatistics(result)); + AppEvents.SetDefaultServerRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async indexId => await SetDefaultServer(indexId)); + #endregion AppEvents _ = Init(); @@ -266,7 +281,7 @@ public class ProfilesViewModel : MyReactiveObject private void Reload() { - Locator.Current.GetService()?.Reload(); + AppEvents.ReloadRequested.Publish(); } public async Task SetSpeedTestResult(SpeedTestResult result) @@ -285,14 +300,14 @@ public class ProfilesViewModel : MyReactiveObject if (result.Delay.IsNotEmpty()) { - int.TryParse(result.Delay, out var temp); - item.Delay = temp; + item.Delay = result.Delay.ToInt(); item.DelayVal = result.Delay ?? string.Empty; } if (result.Speed.IsNotEmpty()) { item.SpeedVal = result.Speed ?? string.Empty; } + await Task.CompletedTask; } public async Task UpdateStatistics(ServerSpeedItem update) @@ -318,6 +333,7 @@ public class ProfilesViewModel : MyReactiveObject catch { } + await Task.CompletedTask; } #endregion Actions @@ -352,7 +368,7 @@ public class ProfilesViewModel : MyReactiveObject public async Task RefreshServers() { - AppEvents.ProfilesRefreshRequested.OnNext(Unit.Default); + AppEvents.ProfilesRefreshRequested.Publish(); await Task.Delay(200); } @@ -380,7 +396,7 @@ public class ProfilesViewModel : MyReactiveObject await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); } - public async Task RefreshSubscriptions() + private async Task RefreshSubscriptions() { SubItems.Clear(); @@ -472,7 +488,7 @@ public class ProfilesViewModel : MyReactiveObject return lstSelected; } - public async Task EditServerAsync(EConfigType eConfigType) + public async Task EditServerAsync() { if (string.IsNullOrEmpty(SelectedProfile?.IndexId)) { @@ -484,13 +500,17 @@ public class ProfilesViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } - eConfigType = item.ConfigType; + var eConfigType = item.ConfigType; bool? ret = false; if (eConfigType == EConfigType.Custom) { ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); } + else if (eConfigType.IsGroupType()) + { + ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); + } else { ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); @@ -565,7 +585,7 @@ public class ProfilesViewModel : MyReactiveObject await SetDefaultServer(SelectedProfile.IndexId); } - public async Task SetDefaultServer(string? indexId) + private async Task SetDefaultServer(string? indexId) { if (indexId.IsNullOrEmpty()) { @@ -606,7 +626,7 @@ public class ProfilesViewModel : MyReactiveObject await _updateView?.Invoke(EViewAction.ShareServer, url); } - private async Task SetDefaultMultipleServer(ECoreType coreType, EMultipleLoad multipleLoad) + private async Task GenGroupMultipleServer(ECoreType coreType, EMultipleLoad multipleLoad) { var lstSelected = await GetProfileItems(true); if (lstSelected == null) @@ -614,7 +634,7 @@ public class ProfilesViewModel : MyReactiveObject return; } - var ret = await ConfigHandler.AddCustomServer4Multiple(_config, lstSelected, coreType, multipleLoad); + var ret = await ConfigHandler.AddGroupServer4Multiple(_config, lstSelected, coreType, multipleLoad, SelectedSub?.Id); if (ret.Success != true) { NoticeManager.Instance.Enqueue(ResUI.OperationFailed); @@ -639,7 +659,7 @@ public class ProfilesViewModel : MyReactiveObject } _dicHeaderSort.TryAdd(colName, true); - _dicHeaderSort.TryGetValue(colName, out bool asc); + _dicHeaderSort.TryGetValue(colName, out var asc); if (await ConfigHandler.SortServers(_config, _config.SubIndexId, colName, asc) != 0) { return; @@ -715,6 +735,12 @@ public class ProfilesViewModel : MyReactiveObject { SelectedProfiles = ProfileItems; } + else if (actionType == ESpeedActionType.FastRealping) + { + SelectedProfiles = ProfileItems; + actionType = ESpeedActionType.Realping; + } + var lstSelected = await GetProfileItems(false); if (lstSelected == null) { @@ -728,6 +754,7 @@ public class ProfilesViewModel : MyReactiveObject _ = SetSpeedTestResult(result); return Disposable.Empty; }); + await Task.CompletedTask; }); _speedtestService?.RunLoop(actionType, lstSelected); } @@ -745,6 +772,18 @@ public class ProfilesViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } + + var msgs = await ActionPrecheckManager.Instance.Check(item); + if (msgs.Count > 0) + { + foreach (var msg in msgs) + { + NoticeManager.Instance.SendMessage(msg); + } + NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true)); + return; + } + if (blClipboard) { var result = await CoreConfigHandler.GenerateClientConfig(item, null); diff --git a/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs index 758aa8fe..e984ab64 100644 --- a/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs @@ -1,7 +1,3 @@ -using System.Reactive; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.ViewModels; public class RoutingRuleDetailsViewModel : MyReactiveObject @@ -21,6 +17,9 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject [Reactive] public string Process { get; set; } + [Reactive] + public string? RuleType { get; set; } + [Reactive] public bool AutoSort { get; set; } @@ -51,6 +50,7 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject Domain = Utils.List2String(SelectedSource.Domain, true); IP = Utils.List2String(SelectedSource.Ip, true); Process = Utils.List2String(SelectedSource.Process, true); + RuleType = SelectedSource.RuleType?.ToString(); } private async Task SaveRulesAsync() @@ -73,6 +73,7 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject } SelectedSource.Protocol = ProtocolItems?.ToList(); SelectedSource.InboundTag = InboundTagItems?.ToList(); + SelectedSource.RuleType = RuleType.IsNullOrEmpty() ? null : (ERuleType)Enum.Parse(typeof(ERuleType), RuleType); var hasRule = SelectedSource.Domain?.Count > 0 || SelectedSource.Ip?.Count > 0 diff --git a/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs index 4b192f04..71d42218 100644 --- a/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs @@ -1,10 +1,3 @@ -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 @@ -107,13 +100,13 @@ public class RoutingRuleSettingViewModel : MyReactiveObject var it = new RulesItemModel() { Id = item.Id, + RuleTypeName = item.RuleType?.ToString(), 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), + Domains = Utils.List2String((item.Domain ?? []).Concat(item.Ip ?? []).ToList()), Enabled = item.Enabled, Remarks = item.Remarks, }; @@ -222,7 +215,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject private async Task SaveRoutingAsync() { - string remarks = SelectedRouting.Remarks; + var remarks = SelectedRouting.Remarks; if (remarks.IsNullOrEmpty()) { NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); @@ -293,7 +286,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject return; } - DownloadService downloadHandle = new DownloadService(); + var downloadHandle = new DownloadService(); var result = await downloadHandle.TryDownloadString(url, true, ""); var ret = await AddBatchRoutingRulesAsync(SelectedRouting, result); if (ret == 0) @@ -305,7 +298,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject private async Task AddBatchRoutingRulesAsync(RoutingItem routingItem, string? clipboardData) { - bool blReplace = false; + var blReplace = false; if (await _updateView?.Invoke(EViewAction.AddBatchRoutingRulesYesNo, null) == false) { blReplace = true; diff --git a/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs index 5237a8d2..8f62f2d0 100644 --- a/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs @@ -1,8 +1,3 @@ -using System.Reactive; -using DynamicData.Binding; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.ViewModels; public class RoutingSettingViewModel : MyReactiveObject diff --git a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs index e9ee033e..7617ac6f 100644 --- a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs @@ -1,16 +1,10 @@ -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 { + private static readonly Lazy _instance = new(() => new(null)); + public static StatusBarViewModel Instance => _instance.Value; + #region ObservableCollection public IObservableCollection RoutingItems { get; } = new ObservableCollectionExtended(); @@ -126,7 +120,7 @@ public class StatusBarViewModel : MyReactiveObject this.WhenAnyValue( x => x.SelectedServer, y => y != null && !y.Text.IsNullOrEmpty()) - .Subscribe(c => ServerSelectedChanged(c)); + .Subscribe(ServerSelectedChanged); SystemProxySelected = (int)_config.SystemProxyItem.SysProxyType; this.WhenAnyValue( @@ -146,17 +140,17 @@ public class StatusBarViewModel : MyReactiveObject NotifyLeftClickCmd = ReactiveCommand.CreateFromTask(async () => { - Locator.Current.GetService()?.ShowHideWindow(null); + AppEvents.ShowHideWindowRequested.Publish(null); await Task.CompletedTask; }); ShowWindowCmd = ReactiveCommand.CreateFromTask(async () => { - Locator.Current.GetService()?.ShowHideWindow(true); + AppEvents.ShowHideWindowRequested.Publish(true); await Task.CompletedTask; }); HideWindowCmd = ReactiveCommand.CreateFromTask(async () => { - Locator.Current.GetService()?.ShowHideWindow(false); + AppEvents.ShowHideWindowRequested.Publish(false); await Task.CompletedTask; }); @@ -209,6 +203,26 @@ public class StatusBarViewModel : MyReactiveObject .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(async result => await UpdateStatistics(result)); + AppEvents.RoutingsMenuRefreshRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await RefreshRoutingsMenu()); + + AppEvents.TestServerRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await TestServerAvailability()); + + AppEvents.InboundDisplayRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await InboundDisplayStatus()); + + AppEvents.SysProxyChangeRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async result => await SetListenerType(result)); + #endregion AppEvents _ = Init(); @@ -216,6 +230,7 @@ public class StatusBarViewModel : MyReactiveObject private async Task Init() { + await ConfigHandler.InitBuiltinRouting(_config); await RefreshRoutingsMenu(); await InboundDisplayStatus(); await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, true); @@ -252,23 +267,20 @@ public class StatusBarViewModel : MyReactiveObject private async Task AddServerViaClipboard() { - var service = Locator.Current.GetService(); - if (service != null) - await service.AddServerViaClipboardAsync(null); + AppEvents.AddServerViaClipboardRequested.Publish(); + await Task.Delay(1000); } private async Task AddServerViaScan() { - var service = Locator.Current.GetService(); - if (service != null) - await service.AddServerViaScanAsync(); + AppEvents.AddServerViaScanRequested.Publish(); + await Task.Delay(1000); } private async Task UpdateSubscriptionProcess(bool blProxy) { - var service = Locator.Current.GetService(); - if (service != null) - await service.UpdateSubscriptionProcess("", blProxy); + AppEvents.SubscriptionsUpdateRequested.Publish(blProxy); + await Task.Delay(1000); } private async Task RefreshServersBiz() @@ -301,10 +313,10 @@ public class StatusBarViewModel : MyReactiveObject } BlServers = true; - for (int k = 0; k < lstModel.Count; k++) + for (var k = 0; k < lstModel.Count; k++) { ProfileItem it = lstModel[k]; - string name = it.GetSummary(); + var name = it.GetSummary(); var item = new ComboItem() { ID = it.IndexId, Text = name }; Servers.Add(item); @@ -329,7 +341,7 @@ public class StatusBarViewModel : MyReactiveObject { return; } - Locator.Current.GetService()?.SetDefaultServer(SelectedServer.ID); + AppEvents.SetDefaultServerRequested.Publish(SelectedServer.ID); } public async Task TestServerAvailability() @@ -355,16 +367,18 @@ public class StatusBarViewModel : MyReactiveObject _ = TestServerAvailabilityResult(msg); return Disposable.Empty; }); + await Task.CompletedTask; } public async Task TestServerAvailabilityResult(string msg) { RunningInfoDisplay = msg; + await Task.CompletedTask; } #region System proxy and Routings - public async Task SetListenerType(ESysProxyType type) + private async Task SetListenerType(ESysProxyType type) { if (_config.SystemProxyItem.SysProxyType == type) { @@ -372,7 +386,7 @@ public class StatusBarViewModel : MyReactiveObject } _config.SystemProxyItem.SysProxyType = type; await ChangeSystemProxyAsync(type, true); - NoticeManager.Instance.SendMessageEx($"{ResUI.TipChangeSystemProxy} - {_config.SystemProxyItem.SysProxyType.ToString()}"); + NoticeManager.Instance.SendMessageEx($"{ResUI.TipChangeSystemProxy} - {_config.SystemProxyItem.SysProxyType}"); SystemProxySelected = (int)_config.SystemProxyItem.SysProxyType; await ConfigHandler.SaveConfig(_config); @@ -382,10 +396,10 @@ public class StatusBarViewModel : MyReactiveObject { await SysProxyHandler.UpdateSysProxy(_config, false); - BlSystemProxyClear = (type == ESysProxyType.ForcedClear); - BlSystemProxySet = (type == ESysProxyType.ForcedChange); - BlSystemProxyNothing = (type == ESysProxyType.Unchanged); - BlSystemProxyPac = (type == ESysProxyType.Pac); + BlSystemProxyClear = type == ESysProxyType.ForcedClear; + BlSystemProxySet = type == ESysProxyType.ForcedChange; + BlSystemProxyNothing = type == ESysProxyType.Unchanged; + BlSystemProxyPac = type == ESysProxyType.Pac; if (blChange) { @@ -393,7 +407,7 @@ public class StatusBarViewModel : MyReactiveObject } } - public async Task RefreshRoutingsMenu() + private async Task RefreshRoutingsMenu() { RoutingItems.Clear(); @@ -430,7 +444,7 @@ public class StatusBarViewModel : MyReactiveObject if (await ConfigHandler.SetDefaultRouting(_config, item) == 0) { NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting); - Locator.Current.GetService()?.Reload(); + AppEvents.ReloadRequested.Publish(); _updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null); } } @@ -463,7 +477,7 @@ public class StatusBarViewModel : MyReactiveObject if (Utils.IsWindows()) { _config.TunModeItem.EnableTun = false; - Locator.Current.GetService()?.RebootAsAdmin(); + await AppManager.Instance.RebootAsAdmin(); return; } else @@ -477,7 +491,7 @@ public class StatusBarViewModel : MyReactiveObject } } await ConfigHandler.SaveConfig(_config); - Locator.Current.GetService()?.Reload(); + AppEvents.ReloadRequested.Publish(); } private bool AllowEnableTun() @@ -490,7 +504,7 @@ public class StatusBarViewModel : MyReactiveObject { return AppManager.Instance.LinuxSudoPwd.IsNotEmpty(); } - else if (Utils.IsOSX()) + else if (Utils.IsMacOS()) { return AppManager.Instance.LinuxSudoPwd.IsNotEmpty(); } @@ -501,7 +515,7 @@ public class StatusBarViewModel : MyReactiveObject #region UI - public async Task InboundDisplayStatus() + private async Task InboundDisplayStatus() { StringBuilder sb = new(); sb.Append($"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}"); @@ -549,6 +563,7 @@ public class StatusBarViewModel : MyReactiveObject catch { } + await Task.CompletedTask; } #endregion UI diff --git a/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs b/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs index bfbfbbe7..344f4ac8 100644 --- a/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs @@ -1,7 +1,3 @@ -using System.Reactive; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.ViewModels; public class SubEditViewModel : MyReactiveObject diff --git a/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs index 88f33619..ce0f6fa7 100644 --- a/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs @@ -1,9 +1,3 @@ -using System.Reactive; -using DynamicData; -using DynamicData.Binding; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; - namespace ServiceLib.ViewModels; public class SubSettingViewModel : MyReactiveObject diff --git a/v2rayN/v2rayN.Desktop/App.axaml b/v2rayN/v2rayN.Desktop/App.axaml index 8b61125f..21204f60 100644 --- a/v2rayN/v2rayN.Desktop/App.axaml +++ b/v2rayN/v2rayN.Desktop/App.axaml @@ -8,21 +8,21 @@ xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" Name="v2rayN" x:DataType="vms:StatusBarViewModel" - RequestedThemeVariant="Default"> - - - - - - + RequestedThemeVariant="Default"> - + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/App.axaml.cs b/v2rayN/v2rayN.Desktop/App.axaml.cs index aa357ca4..35371f0f 100644 --- a/v2rayN/v2rayN.Desktop/App.axaml.cs +++ b/v2rayN/v2rayN.Desktop/App.axaml.cs @@ -1,8 +1,3 @@ -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Markup.Xaml; -using Splat; -using v2rayN.Desktop.Common; using v2rayN.Desktop.Views; namespace v2rayN.Desktop; @@ -15,17 +10,17 @@ public partial class App : Application 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(); + if (!Design.IsDesignMode) + { + AppManager.Instance.InitComponents(); + DataContext = StatusBarViewModel.Instance; + } desktop.Exit += OnExit; desktop.MainWindow = new MainWindow(); @@ -57,16 +52,8 @@ public partial class App : Application { 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); - } + AppEvents.AddServerViaClipboardRequested.Publish(); + await Task.Delay(1000); } } } diff --git a/v2rayN/v2rayN.Desktop/Assets/GlobalStyles.axaml b/v2rayN/v2rayN.Desktop/Assets/GlobalStyles.axaml index 6a2cfacd..eccdeb91 100644 --- a/v2rayN/v2rayN.Desktop/Assets/GlobalStyles.axaml +++ b/v2rayN/v2rayN.Desktop/Assets/GlobalStyles.axaml @@ -13,6 +13,7 @@ + + + diff --git a/v2rayN/v2rayN.Desktop/Base/WindowBase.cs b/v2rayN/v2rayN.Desktop/Base/WindowBase.cs index 10105d2e..d1419b30 100644 --- a/v2rayN/v2rayN.Desktop/Base/WindowBase.cs +++ b/v2rayN/v2rayN.Desktop/Base/WindowBase.cs @@ -1,7 +1,3 @@ -using Avalonia; -using Avalonia.Interactivity; -using Avalonia.ReactiveUI; - namespace v2rayN.Desktop.Base; public class WindowBase : ReactiveWindow where TViewModel : class @@ -30,7 +26,7 @@ public class WindowBase : ReactiveWindow where TViewMode Height = sizeItem.Height; var workingArea = (Screens.ScreenFromWindow(this) ?? Screens.Primary).WorkingArea; - var scaling = (Utils.IsOSX() ? null : VisualRoot?.RenderScaling) ?? 1.0; + var scaling = (Utils.IsMacOS() ? null : VisualRoot?.RenderScaling) ?? 1.0; var x = workingArea.X + ((workingArea.Width - (Width * scaling)) / 2); var y = workingArea.Y + ((workingArea.Height - (Height * scaling)) / 2); diff --git a/v2rayN/v2rayN.Desktop/Common/AppBuilderExtension.cs b/v2rayN/v2rayN.Desktop/Common/AppBuilderExtension.cs index 3c5169cc..c0702938 100644 --- a/v2rayN/v2rayN.Desktop/Common/AppBuilderExtension.cs +++ b/v2rayN/v2rayN.Desktop/Common/AppBuilderExtension.cs @@ -1,6 +1,3 @@ -using Avalonia; -using Avalonia.Media; - namespace v2rayN.Desktop.Common; public static class AppBuilderExtension diff --git a/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs b/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs index 87c974d4..cca01643 100644 --- a/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs +++ b/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs @@ -1,8 +1,4 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Media.Imaging; -using Avalonia.Platform; +using Avalonia.Input.Platform; namespace v2rayN.Desktop.Common; @@ -18,7 +14,7 @@ internal class AvaUtils return null; } - return await clipboard.GetTextAsync(); + return await clipboard.TryGetTextAsync(); } catch { @@ -32,10 +28,11 @@ internal class AvaUtils { var clipboard = TopLevel.GetTopLevel(visual)?.Clipboard; if (clipboard == null) + { return; - var dataObject = new DataObject(); - dataObject.Set(DataFormats.Text, strData); - await clipboard.SetDataObjectAsync(dataObject); + } + + await clipboard.SetTextAsync(strData); } catch { diff --git a/v2rayN/v2rayN.Desktop/Common/TextEditorKeywordHighlighter.cs b/v2rayN/v2rayN.Desktop/Common/TextEditorKeywordHighlighter.cs new file mode 100644 index 00000000..f5e4dc6a --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Common/TextEditorKeywordHighlighter.cs @@ -0,0 +1,128 @@ +using AvaloniaEdit; +using AvaloniaEdit.Document; +using AvaloniaEdit.Rendering; + +namespace v2rayN.Desktop.Common; + +public class KeywordColorizer : DocumentColorizingTransformer +{ + private readonly string[] _keywords; + private readonly Dictionary _brushMap; + + public KeywordColorizer(IDictionary keywordBrushMap) + { + if (keywordBrushMap == null || keywordBrushMap.Count == 0) + { + throw new ArgumentException("keywordBrushMap must not be null or empty", nameof(keywordBrushMap)); + } + + _brushMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in keywordBrushMap) + { + if (string.IsNullOrEmpty(kvp.Key) || kvp.Value == null) + { + continue; + } + + if (!_brushMap.ContainsKey(kvp.Key)) + { + _brushMap[kvp.Key] = kvp.Value; + } + } + + if (_brushMap.Count == 0) + { + throw new ArgumentException("keywordBrushMap must contain at least one non-empty key with a non-null brush", nameof(keywordBrushMap)); + } + + _keywords = _brushMap.Keys.ToArray(); + } + + protected override void ColorizeLine(DocumentLine line) + { + var text = CurrentContext.Document.GetText(line); + if (string.IsNullOrEmpty(text)) + { + return; + } + + foreach (var kw in _keywords) + { + if (string.IsNullOrEmpty(kw)) + { + continue; + } + + var searchStart = 0; + while (true) + { + var idx = text.IndexOf(kw, searchStart, StringComparison.OrdinalIgnoreCase); + if (idx < 0) + { + break; + } + + var kwEndIndex = idx + kw.Length; + if (IsWordCharBefore(text, idx) || IsWordCharAfter(text, kwEndIndex)) + { + searchStart = idx + Math.Max(1, kw.Length); + continue; + } + + var start = line.Offset + idx; + var end = start + kw.Length; + + if (_brushMap.TryGetValue(kw, out var brush) && brush != null) + { + ChangeLinePart(start, end, element => element.TextRunProperties.SetForegroundBrush(brush)); + } + + searchStart = idx + Math.Max(1, kw.Length); + } + } + } + + private static bool IsWordCharBefore(string text, int idx) + { + if (idx <= 0) + { + return false; + } + + var c = text[idx - 1]; + return char.IsLetterOrDigit(c) || c == '_'; + } + + private static bool IsWordCharAfter(string text, int idx) + { + if (idx >= text.Length) + { + return false; + } + + var c = text[idx]; + return char.IsLetterOrDigit(c) || c == '_'; + } +} + +public static class TextEditorKeywordHighlighter +{ + public static void Attach(TextEditor editor, IDictionary keywordBrushMap) + { + ArgumentNullException.ThrowIfNull(editor); + + if (keywordBrushMap == null || keywordBrushMap.Count == 0) + { + return; + } + + if (editor.TextArea?.TextView?.LineTransformers?.OfType().Any() == true) + { + return; + } + + var colorizer = new KeywordColorizer(keywordBrushMap); + editor.TextArea.TextView.LineTransformers.Add(colorizer); + editor.TextArea.TextView.InvalidateVisual(); + } +} diff --git a/v2rayN/v2rayN.Desktop/Common/UI.cs b/v2rayN/v2rayN.Desktop/Common/UI.cs index e9ddc36f..c17d8947 100644 --- a/v2rayN/v2rayN.Desktop/Common/UI.cs +++ b/v2rayN/v2rayN.Desktop/Common/UI.cs @@ -1,7 +1,5 @@ -using Avalonia.Controls; using Avalonia.Platform.Storage; using MsBox.Avalonia; -using MsBox.Avalonia.Enums; namespace v2rayN.Desktop.Common; diff --git a/v2rayN/v2rayN.Desktop/Controls/AutoCompleteBox.axaml b/v2rayN/v2rayN.Desktop/Controls/AutoCompleteBox.axaml deleted file mode 100644 index 97fec908..00000000 --- a/v2rayN/v2rayN.Desktop/Controls/AutoCompleteBox.axaml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/v2rayN/v2rayN.Desktop/Controls/AutoCompleteBox.cs b/v2rayN/v2rayN.Desktop/Controls/AutoCompleteBox.cs deleted file mode 100644 index 82e04d4b..00000000 --- a/v2rayN/v2rayN.Desktop/Controls/AutoCompleteBox.cs +++ /dev/null @@ -1,40 +0,0 @@ -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 index 05364578..7b339aea 100644 --- a/v2rayN/v2rayN.Desktop/Converters/DelayColorConverter.cs +++ b/v2rayN/v2rayN.Desktop/Converters/DelayColorConverter.cs @@ -1,6 +1,4 @@ -using System.Globalization; using Avalonia.Data.Converters; -using Avalonia.Media; namespace v2rayN.Desktop.Converters; @@ -8,7 +6,7 @@ public class DelayColorConverter : IValueConverter { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - _ = int.TryParse(value?.ToString(), out var delay); + var delay = value.ToString().ToInt(); return delay switch { diff --git a/v2rayN/v2rayN.Desktop/GlobalUsings.cs b/v2rayN/v2rayN.Desktop/GlobalUsings.cs index 4f2b931d..e7499b01 100644 --- a/v2rayN/v2rayN.Desktop/GlobalUsings.cs +++ b/v2rayN/v2rayN.Desktop/GlobalUsings.cs @@ -1,7 +1,34 @@ +global using System; +global using System.Collections.Generic; +global using System.Globalization; +global using System.IO; +global using System.Linq; +global using System.Reactive.Disposables.Fluent; +global using System.Reactive.Linq; +global using System.Text; +global using System.Threading; +global using System.Threading.Tasks; +global using Avalonia; +global using Avalonia.Controls; +global using Avalonia.Controls.ApplicationLifetimes; +global using Avalonia.Input; +global using Avalonia.Interactivity; +global using Avalonia.Markup.Xaml; +global using Avalonia.Media; +global using Avalonia.Media.Imaging; +global using Avalonia.Platform; +global using Avalonia.Styling; +global using Avalonia.Threading; +global using DynamicData; +global using MsBox.Avalonia.Enums; +global using ReactiveUI; +global using ReactiveUI.Avalonia; +global using ReactiveUI.Fody.Helpers; global using ServiceLib; global using ServiceLib.Base; global using ServiceLib.Common; global using ServiceLib.Enums; +global using ServiceLib.Events; global using ServiceLib.Handler; global using ServiceLib.Manager; global using ServiceLib.Models; diff --git a/v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs b/v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs index 5ce6ff60..f803c538 100644 --- a/v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs +++ b/v2rayN/v2rayN.Desktop/Manager/HotkeyManager.cs @@ -1,6 +1,3 @@ -using System.Reactive.Linq; -using Avalonia.Input; -using Avalonia.ReactiveUI; using Avalonia.Win32.Input; using GlobalHotKeys; @@ -11,7 +8,7 @@ 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 GlobalHotKeys.HotKeyManager? _hotKeyManager; private Config? _config; diff --git a/v2rayN/v2rayN.Desktop/Program.cs b/v2rayN/v2rayN.Desktop/Program.cs index 4b534324..70c43130 100644 --- a/v2rayN/v2rayN.Desktop/Program.cs +++ b/v2rayN/v2rayN.Desktop/Program.cs @@ -1,5 +1,3 @@ -using Avalonia; -using Avalonia.ReactiveUI; using v2rayN.Desktop.Common; namespace v2rayN.Desktop; @@ -56,12 +54,19 @@ internal class Program // 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 }); + var builder = AppBuilder.Configure() + .UsePlatformDetect() + //.WithInterFont() + .WithFontByDefault() + .LogToTrace() + .UseReactiveUI(); + + if (OperatingSystem.IsMacOS()) + { + var showInDock = Design.IsDesignMode || AppManager.Instance.Config.UiItem.MacOSShowInDock; + builder = builder.With(new MacOSPlatformOptions { ShowInDock = showInDock }); + } + + return builder; } } diff --git a/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs b/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs index 3473cf23..c84a2887 100644 --- a/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs +++ b/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs @@ -1,12 +1,6 @@ -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 AvaloniaEdit; using Semi.Avalonia; namespace v2rayN.Desktop.ViewModels; @@ -101,7 +95,9 @@ public class ThemeSettingViewModel : MyReactiveObject { double size = CurrentFontSize; if (size < Global.MinFontSize) + { return; + } Style style = new(x => Selectors.Or( x.OfType + { InitializeComponent(); - this.Loaded += Window_Loaded; - btnCancel.Click += (s, e) => this.Close(); + Loaded += Window_Loaded; + btnCancel.Click += (s, e) => Close(); cmbNetwork.SelectionChanged += CmbNetwork_SelectionChanged; cmbStreamSecurity.SelectionChanged += CmbStreamSecurity_SelectionChanged; btnGUID.Click += btnGUID_Click; @@ -189,6 +185,8 @@ public partial class AddServerWindow : WindowBase 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); + this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).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); @@ -197,10 +195,12 @@ public partial class AddServerWindow : WindowBase 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.FetchCertCmd, v => v.btnFetchCert).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); }); - this.Title = $"{profileItem.ConfigType}"; + Title = $"{profileItem.ConfigType}"; } private async Task UpdateViewHandler(EViewAction action, object? obj) @@ -208,7 +208,7 @@ public partial class AddServerWindow : WindowBase switch (action) { case EViewAction.CloseWindow: - this.Close(true); + Close(true); break; } return await Task.FromResult(true); diff --git a/v2rayN/v2rayN.Desktop/Views/BackupAndRestoreView.axaml b/v2rayN/v2rayN.Desktop/Views/BackupAndRestoreView.axaml index 7130c400..70d6b9a9 100644 --- a/v2rayN/v2rayN.Desktop/Views/BackupAndRestoreView.axaml +++ b/v2rayN/v2rayN.Desktop/Views/BackupAndRestoreView.axaml @@ -88,13 +88,14 @@ diff --git a/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs index 76a0fd1d..1c39df72 100644 --- a/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/ClashConnectionsView.axaml.cs @@ -1,9 +1,3 @@ -using System.Reactive.Disposables; -using Avalonia.Controls; -using Avalonia.Interactivity; -using Avalonia.ReactiveUI; -using ReactiveUI; - namespace v2rayN.Desktop.Views; public partial class ClashConnectionsView : ReactiveUserControl diff --git a/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml b/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml index 974d5192..a4ef8315 100644 --- a/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml +++ b/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml @@ -50,27 +50,21 @@ diff --git a/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml.cs index 754dd2a1..42068af4 100644 --- a/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/ClashProxiesView.axaml.cs @@ -1,10 +1,3 @@ -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 @@ -13,9 +6,8 @@ public partial class ClashProxiesView : ReactiveUserControl ViewModel, typeof(ClashProxiesViewModel)); lstProxyDetails.DoubleTapped += LstProxyDetails_DoubleTapped; - this.KeyDown += ClashProxiesView_KeyDown; + KeyDown += ClashProxiesView_KeyDown; this.WhenActivated(disposables => { diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml index d8eed12e..e5a785b7 100644 --- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml @@ -2,7 +2,6 @@ x:Class="v2rayN.Desktop.Views.DNSSettingWindow" xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:ctrls="clr-namespace:v2rayN.Desktop.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" @@ -37,6 +36,7 @@ @@ -55,13 +55,13 @@ Margin="{StaticResource Margin4}" VerticalAlignment="Center" Text="{x:Static resx:ResUI.TbDomesticDNS}" /> - + IsEditable="True" /> - - + IsEditable="True" /> + + - + + IsEditable="True" /> - - - - + Text="{x:Static resx:ResUI.TbXrayFreedomStrategy}" /> + + + - - - - @@ -187,6 +166,7 @@ @@ -258,13 +238,13 @@ Margin="{StaticResource Margin4}" VerticalAlignment="Center" Text="{x:Static resx:ResUI.TbValidateDirectExpectedIPs}" /> - + IsEditable="True" /> - + @@ -361,11 +341,11 @@ Margin="{StaticResource Margin4}" VerticalAlignment="Center" Text="{x:Static resx:ResUI.TbSettingsDomainDNSAddress}" /> - + IsEditable="True" /> @@ -384,7 +364,7 @@ - + @@ -433,11 +413,11 @@ Margin="{StaticResource Margin4}" VerticalAlignment="Center" Text="{x:Static resx:ResUI.TbSettingsDomainDNSAddress}" /> - + IsEditable="True" /> diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs index 2718df95..e673a025 100644 --- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs @@ -1,7 +1,3 @@ -using System.Reactive.Disposables; -using Avalonia.Controls; -using Avalonia.Interactivity; -using ReactiveUI; using v2rayN.Desktop.Base; namespace v2rayN.Desktop.Views; @@ -16,16 +12,15 @@ public partial class DNSSettingWindow : WindowBase _config = AppManager.Instance.Config; Loaded += Window_Loaded; - btnCancel.Click += (s, e) => this.Close(); + btnCancel.Click += (s, e) => Close(); ViewModel = new DNSSettingViewModel(UpdateViewHandler); cmbRayFreedomDNSStrategy.ItemsSource = Global.DomainStrategy4Freedoms; cmbSBDirectDNSStrategy.ItemsSource = Global.SingboxDomainStrategy4Out; cmbSBRemoteDNSStrategy.ItemsSource = Global.SingboxDomainStrategy4Out; cmbDirectDNS.ItemsSource = Global.DomainDirectDNSAddress; - cmbSBResolverDNS.ItemsSource = Global.DomainDirectDNSAddress.Concat(new[] { "dhcp://auto,localhost" }); cmbRemoteDNS.ItemsSource = Global.DomainRemoteDNSAddress; - cmbSBFinalResolverDNS.ItemsSource = Global.DomainPureIPDNSAddress.Concat(new[] { "dhcp://auto,localhost" }); + cmbBootstrapDNS.ItemsSource = Global.DomainPureIPDNSAddress; cmbDirectExpectedIPs.ItemsSource = Global.ExpectedIPs; cmbdomainStrategy4FreedomCompatible.ItemsSource = Global.DomainStrategy4Freedoms; @@ -39,15 +34,14 @@ public partial class DNSSettingWindow : WindowBase this.Bind(ViewModel, vm => vm.AddCommonHosts, v => v.togAddCommonHosts.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.FakeIP, v => v.togFakeIP.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.BlockBindingQuery, v => v.togBlockBindingQuery.IsChecked).DisposeWith(disposables); - //this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.Text).DisposeWith(disposables); - //this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables); - //this.Bind(ViewModel, vm => vm.SingboxOutboundsResolveDNS, v => v.cmbSBResolverDNS.Text).DisposeWith(disposables); - //this.Bind(ViewModel, vm => vm.SingboxFinalResolveDNS, v => v.cmbSBFinalResolverDNS.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.BootstrapDNS, v => v.cmbBootstrapDNS.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.RayStrategy4Freedom, v => v.cmbRayFreedomDNSStrategy.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SingboxStrategy4Direct, v => v.cmbSBDirectDNSStrategy.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SingboxStrategy4Proxy, v => v.cmbSBRemoteDNSStrategy.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Hosts, v => v.txtHosts.Text).DisposeWith(disposables); - //this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.Text).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); @@ -56,27 +50,25 @@ public partial class DNSSettingWindow : WindowBase this.Bind(ViewModel, vm => vm.UseSystemHostsCompatible, v => v.togUseSystemHostsCompatible.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DomainStrategy4FreedomCompatible, v => v.cmbdomainStrategy4FreedomCompatible.SelectedItem).DisposeWith(disposables); - //this.Bind(ViewModel, vm => vm.DomainDNSAddressCompatible, v => v.cmbdomainDNSAddressCompatible.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.DomainDNSAddressCompatible, v => v.cmbdomainDNSAddressCompatible.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.NormalDNSCompatible, v => v.txtnormalDNSCompatible.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.DomainStrategy4Freedom2Compatible, v => v.cmbdomainStrategy4OutCompatible.SelectedItem).DisposeWith(disposables); - //this.Bind(ViewModel, vm => vm.DomainDNSAddress2Compatible, v => v.cmbdomainDNSAddress2Compatible.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.DomainDNSAddress2Compatible, v => v.cmbdomainDNSAddress2Compatible.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.NormalDNS2Compatible, v => v.txtnormalDNS2Compatible.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunDNS2Compatible, v => v.txttunDNS2Compatible.Text).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ImportDefConfig4V2rayCompatibleCmd, v => v.btnImportDefConfig4V2rayCompatible).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ImportDefConfig4SingboxCompatibleCmd, v => v.btnImportDefConfig4SingboxCompatible).DisposeWith(disposables); - this.WhenAnyValue( - x => x.ViewModel.RayCustomDNSEnableCompatible, - x => x.ViewModel.SBCustomDNSEnableCompatible, - (ray, sb) => ray && sb - ).BindTo(this.FindControl("txtBasicDNSSettingsInvalid"), t => t.IsVisible); - this.WhenAnyValue( - x => x.ViewModel.RayCustomDNSEnableCompatible, - x => x.ViewModel.SBCustomDNSEnableCompatible, - (ray, sb) => ray && sb - ).BindTo(this.FindControl("txtAdvancedDNSSettingsInvalid"), t => t.IsVisible); + this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled) + .Select(b => !b) + .BindTo(this.FindControl("txtBasicDNSSettingsInvalid"), t => t.IsVisible); + this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled) + .Select(b => !b) + .BindTo(this.FindControl("txtAdvancedDNSSettingsInvalid"), t => t.IsVisible); + this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridBasicDNSSettings.IsEnabled).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridAdvancedDNSSettings.IsEnabled).DisposeWith(disposables); }); } @@ -85,7 +77,7 @@ public partial class DNSSettingWindow : WindowBase switch (action) { case EViewAction.CloseWindow: - this.Close(true); + Close(true); break; } return await Task.FromResult(true); diff --git a/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs index 97bc4afb..bfe0c2c5 100644 --- a/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/FullConfigTemplateWindow.axaml.cs @@ -1,6 +1,3 @@ -using System.Reactive.Disposables; -using Avalonia.Interactivity; -using ReactiveUI; using v2rayN.Desktop.Base; namespace v2rayN.Desktop.Views; @@ -15,7 +12,7 @@ public partial class FullConfigTemplateWindow : WindowBase this.Close(); + btnCancel.Click += (s, e) => Close(); ViewModel = new FullConfigTemplateViewModel(UpdateViewHandler); this.WhenActivated(disposables => @@ -39,7 +36,7 @@ public partial class FullConfigTemplateWindow : WindowBase HotkeyManager.Instance.IsPause = false; - btnCancel.Click += (s, e) => this.Close(); + Closing += (s, e) => HotkeyManager.Instance.IsPause = false; + btnCancel.Click += (s, e) => Close(); this.WhenActivated(disposables => { @@ -40,7 +34,7 @@ public partial class GlobalHotkeySettingWindow : WindowBase - + @@ -35,6 +35,8 @@ + + @@ -49,7 +51,7 @@ - + @@ -63,7 +65,7 @@ - + @@ -73,10 +75,10 @@ - + - - + + @@ -88,31 +90,15 @@ - - - - - - - + - - - - - - - + - - - - - - - + + + diff --git a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs index ffac047d..15e2cded 100644 --- a/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/MainWindow.axaml.cs @@ -1,16 +1,5 @@ -using System.Reactive.Disposables; -using System.Reactive.Linq; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Threading; using DialogHostAvalonia; -using MsBox.Avalonia.Enums; -using ReactiveUI; -using Splat; using v2rayN.Desktop.Base; using v2rayN.Desktop.Common; using v2rayN.Desktop.Manager; @@ -20,7 +9,7 @@ namespace v2rayN.Desktop.Views; public partial class MainWindow : WindowBase { private static Config _config; - private WindowNotificationManager? _manager; + private readonly WindowNotificationManager? _manager; private CheckUpdateView? _checkUpdateView; private BackupAndRestoreView? _backupAndRestoreView; private bool _blCloseByUser = false; @@ -32,15 +21,14 @@ public partial class MainWindow : WindowBase _config = AppManager.Instance.Config; _manager = new WindowNotificationManager(TopLevel.GetTopLevel(this)) { MaxItems = 3, Position = NotificationPosition.TopRight }; - this.KeyDown += MainWindow_KeyDown; - menuSettingsSetUWP.Click += menuSettingsSetUWP_Click; - //menuPromotion.Click += menuPromotion_Click; + KeyDown += MainWindow_KeyDown; + menuSettingsSetUWP.Click += MenuSettingsSetUWP_Click; + // menuPromotion.Click += MenuPromotion_Click; menuCheckUpdate.Click += MenuCheckUpdate_Click; menuBackupAndRestore.Click += MenuBackupAndRestore_Click; menuClose.Click += MenuClose_Click; ViewModel = new MainWindowViewModel(UpdateViewHandler); - Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(MainWindowViewModel)); switch (_config.UiItem.MainGirdOrientation) { @@ -85,6 +73,8 @@ public partial class MainWindow : WindowBase this.BindCommand(ViewModel, vm => vm.AddWireguardServerCmd, v => v.menuAddWireguardServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.AddProxyChainServerCmd, v => v.menuAddProxyChainServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaClipboardCmd, v => v.menuAddServerViaClipboard).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaScanCmd, v => v.menuAddServerViaScan).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaImageCmd, v => v.menuAddServerViaImage).DisposeWith(disposables); @@ -153,25 +143,32 @@ public partial class MainWindow : WindowBase .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(content => Shutdown(content)) .DisposeWith(disposables); + + AppEvents.ShowHideWindowRequested + .AsObservable() + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(blShow => ShowHideWindow(blShow)) + .DisposeWith(disposables); }); if (Utils.IsWindows()) { - this.Title = $"{Utils.GetVersion()} - {(Utils.IsAdministrator() ? ResUI.RunAsAdmin : ResUI.NotRunAsAdmin)}"; + Title = $"{Utils.GetVersion()} - {(Utils.IsAdministrator() ? ResUI.RunAsAdmin : ResUI.NotRunAsAdmin)}"; ThreadPool.RegisterWaitForSingleObject(Program.ProgramStarted, OnProgramStarted, null, -1, false); HotkeyManager.Instance.Init(_config, OnHotkeyHandler); } else { - this.Title = $"{Utils.GetVersion()}"; - - menuRebootAsAdmin.IsVisible = false; - menuSettingsSetUWP.IsVisible = false; - menuGlobalHotkeySetting.IsVisible = false; + Title = $"{Utils.GetVersion()}"; } menuAddServerViaScan.IsVisible = false; + if (_config.UiItem.AutoHideStartup && Utils.IsWindows()) + { + WindowState = WindowState.Minimized; + } + AddHelpMenuItem(); } @@ -187,6 +184,7 @@ public partial class MainWindow : WindowBase private async Task DelegateSnackMsg(string content) { _manager?.Show(new Notification(null, content, NotificationType.Information)); + await Task.CompletedTask; } private async Task UpdateViewHandler(EViewAction action, object? obj) @@ -195,14 +193,28 @@ public partial class MainWindow : WindowBase { case EViewAction.AddServerWindow: if (obj is null) + { return false; + } + return await new AddServerWindow((ProfileItem)obj).ShowDialog(this); case EViewAction.AddServer2Window: if (obj is null) + { return false; + } + return await new AddServer2Window((ProfileItem)obj).ShowDialog(this); + case EViewAction.AddGroupServerWindow: + if (obj is null) + { + return false; + } + + return await new AddGroupServerWindow((ProfileItem)obj).ShowDialog(this); + case EViewAction.DNSSettingWindow: return await new DNSSettingWindow().ShowDialog(this); @@ -221,12 +233,6 @@ public partial class MainWindow : WindowBase case EViewAction.SubSettingWindow: return await new SubSettingWindow().ShowDialog(this); - case EViewAction.ShowHideWindow: - Dispatcher.UIThread.Post(() => - ShowHideWindow((bool?)obj), - DispatcherPriority.Default); - break; - case EViewAction.ScanScreenTask: await ScanScreenTaskAsync(); break; @@ -236,11 +242,7 @@ public partial class MainWindow : WindowBase break; case EViewAction.AddServerViaClipboard: - var clipboardData = await AvaUtils.GetClipboardData(this); - if (clipboardData.IsNotEmpty() && ViewModel != null) - { - await ViewModel.AddServerViaClipboardAsync(clipboardData); - } + await AddServerViaClipboardAsync(); break; } @@ -259,7 +261,7 @@ public partial class MainWindow : WindowBase case EGlobalHotkey.SystemProxySet: case EGlobalHotkey.SystemProxyUnchanged: case EGlobalHotkey.SystemProxyPac: - Locator.Current.GetService()?.SetListenerType((ESysProxyType)((int)e - 1)); + AppEvents.SysProxyChangeRequested.Publish((ESysProxyType)((int)e - 1)); break; } } @@ -295,11 +297,7 @@ public partial class MainWindow : WindowBase switch (e.Key) { case Key.V: - var clipboardData = await AvaUtils.GetClipboardData(this); - if (clipboardData.IsNotEmpty() && ViewModel != null) - { - await ViewModel.AddServerViaClipboardAsync(clipboardData); - } + await AddServerViaClipboardAsync(); break; case Key.S: @@ -316,16 +314,32 @@ public partial class MainWindow : WindowBase } } +<<<<<<< HEAD //private void menuPromotion_Click(object? sender, RoutedEventArgs e) //{ // ProcUtils.ProcessStart($"{Utils.Base64Decode(Global.PromotionUrl)}?t={DateTime.Now.Ticks}"); //} +======= + private void MenuPromotion_Click(object? sender, RoutedEventArgs e) + { + ProcUtils.ProcessStart($"{Utils.Base64Decode(Global.PromotionUrl)}?t={DateTime.Now.Ticks}"); + } +>>>>>>> upstream/master - private void menuSettingsSetUWP_Click(object? sender, RoutedEventArgs e) + private void MenuSettingsSetUWP_Click(object? sender, RoutedEventArgs e) { ProcUtils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe")); } + public async Task AddServerViaClipboardAsync() + { + var clipboardData = await AvaUtils.GetClipboardData(this); + if (clipboardData.IsNotEmpty() && ViewModel != null) + { + await ViewModel.AddServerViaClipboardAsync(clipboardData); + } + } + public async Task ScanScreenTaskAsync() { //ShowHideWindow(false); @@ -400,30 +414,33 @@ public partial class MainWindow : WindowBase public void ShowHideWindow(bool? blShow) { - var bl = blShow ?? (!_config.UiItem.ShowInTaskbar ^ (WindowState == WindowState.Minimized)); + var bl = blShow ?? + (Utils.IsLinux() + ? (!_config.UiItem.ShowInTaskbar ^ (WindowState == WindowState.Minimized)) + : !_config.UiItem.ShowInTaskbar); if (bl) { - this.Show(); - if (this.WindowState == WindowState.Minimized) + Show(); + if (WindowState == WindowState.Minimized) { - this.WindowState = WindowState.Normal; + WindowState = WindowState.Normal; } - this.Activate(); - this.Focus(); + Activate(); + Focus(); } else { if (Utils.IsLinux() && _config.UiItem.Hide2TrayWhenClose == false) { - this.WindowState = WindowState.Minimized; + WindowState = WindowState.Minimized; return; } - foreach (var ownedWindow in this.OwnedWindows) + foreach (var ownedWindow in OwnedWindows) { ownedWindow.Close(); } - this.Hide(); + Hide(); } _config.UiItem.ShowInTaskbar = bl; @@ -432,6 +449,10 @@ public partial class MainWindow : WindowBase protected override void OnLoaded(object? sender, RoutedEventArgs e) { base.OnLoaded(sender, e); + if (_config.UiItem.AutoHideStartup) + { + ShowHideWindow(false); + } RestoreUI(); } @@ -470,8 +491,8 @@ public partial class MainWindow : WindowBase { var coreInfo = CoreInfoManager.Instance.GetCoreInfo(); foreach (var it in coreInfo - .Where(t => t.CoreType != ECoreType.v2fly - && t.CoreType != ECoreType.hysteria)) + .Where(t => t.CoreType is not ECoreType.v2fly + and not ECoreType.hysteria)) { var item = new MenuItem() { diff --git a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml index b6ca81a1..8c2cc90c 100644 --- a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml +++ b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml @@ -2,6 +2,7 @@ x:Class="v2rayN.Desktop.Views.MsgView" xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" @@ -24,28 +25,22 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs index e2d21101..78b0a4a1 100644 --- a/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/MsgView.axaml.cs @@ -1,22 +1,15 @@ -using System.Reactive.Disposables; -using Avalonia.Controls; -using Avalonia.Interactivity; -using Avalonia.ReactiveUI; -using Avalonia.Threading; -using ReactiveUI; using v2rayN.Desktop.Common; namespace v2rayN.Desktop.Views; public partial class MsgView : ReactiveUserControl { - private readonly ScrollViewer _scrollViewer; + //private const int KeepLines = 30; public MsgView() { InitializeComponent(); - _scrollViewer = this.FindControl("msgScrollViewer"); - + txtMsg.TextArea.TextView.Options.EnableHyperlinks = false; ViewModel = new MsgViewModel(UpdateViewHandler); this.WhenActivated(disposables => @@ -24,6 +17,11 @@ public partial class MsgView : ReactiveUserControl this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); }); + + TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary( + kv => kv.Key, + kv => (IBrush)new SolidColorBrush(Color.Parse(kv.Value)) + )); } private async Task UpdateViewHandler(EViewAction action, object? obj) @@ -32,11 +30,12 @@ public partial class MsgView : ReactiveUserControl { case EViewAction.DispatcherShowMsg: if (obj is null) + { return false; + } - Dispatcher.UIThread.Post(() => - ShowMsg(obj), - DispatcherPriority.ApplicationIdle); + Dispatcher.UIThread.Post(() => ShowMsg(obj), + DispatcherPriority.ApplicationIdle); break; } return await Task.FromResult(true); @@ -44,23 +43,37 @@ public partial class MsgView : ReactiveUserControl private void ShowMsg(object msg) { - txtMsg.Text = msg.ToString(); + //var lineCount = txtMsg.LineCount; + //if (lineCount > ViewModel?.NumMaxMsg) + //{ + // var cutLine = txtMsg.Document.GetLineByNumber(lineCount - KeepLines); + // txtMsg.Document.Remove(0, cutLine.Offset); + //} + if (txtMsg.LineCount > ViewModel?.NumMaxMsg) + { + ClearMsg(); + } + + txtMsg.AppendText(msg.ToString()); if (togScrollToEnd.IsChecked ?? true) { - _scrollViewer?.ScrollToEnd(); + txtMsg.ScrollToEnd(); } } public void ClearMsg() { - ViewModel?.ClearMsg(); - txtMsg.Text = ""; + txtMsg.Clear(); + txtMsg.AppendText("----- Message cleared -----\n"); } private void menuMsgViewSelectAll_Click(object? sender, RoutedEventArgs e) { - txtMsg.Focus(); - txtMsg.SelectAll(); + Dispatcher.UIThread.Post(() => + { + txtMsg.TextArea.Focus(); + txtMsg.SelectAll(); + }, DispatcherPriority.Render); } private async void menuMsgViewCopy_Click(object? sender, RoutedEventArgs e) diff --git a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml index 3c6e7231..90062951 100644 --- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml @@ -2,7 +2,6 @@ x:Class="v2rayN.Desktop.Views.OptionSettingWindow" xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:ctrls="clr-namespace:v2rayN.Desktop.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" @@ -356,14 +355,15 @@ Grid.Column="1" Margin="{StaticResource Margin4}" HorizontalAlignment="Left" /> - + TextWrapping="Wrap" /> + - - - + HorizontalAlignment="Left" + IsVisible="{Binding BlIsLinux}" /> + + + + - + Margin="{StaticResource Margin4}" + IsEditable="True" /> - + Margin="{StaticResource Margin4}" + IsEditable="True" /> - + Margin="{StaticResource Margin4}" + IsEditable="True" /> - + Margin="{StaticResource Margin4}" + IsEditable="True" /> + Margin="{StaticResource Margin4}" + IsEditable="True" /> + Margin="{StaticResource Margin4}" + IsEditable="True" /> + Margin="{StaticResource Margin4}" + IsEditable="True" /> + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs index d597e567..297c9ae0 100644 --- a/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesSelectWindow.axaml.cs @@ -1,15 +1,9 @@ -using System.Reactive.Disposables; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.ReactiveUI; using Avalonia.VisualTree; -using ReactiveUI; +using v2rayN.Desktop.Base; namespace v2rayN.Desktop.Views; -public partial class ProfilesSelectWindow : ReactiveWindow +public partial class ProfilesSelectWindow : WindowBase { private static Config _config; @@ -96,7 +90,7 @@ public partial class ProfilesSelectWindow : ReactiveWindow() != null) @@ -105,7 +99,7 @@ public partial class ProfilesSelectWindow : ReactiveWindow() != null) { ViewModel?.SelectFinish(); @@ -116,7 +110,7 @@ public partial class ProfilesSelectWindow : ReactiveWindow - + - - @@ -74,6 +55,36 @@ Margin="{StaticResource MarginLr4}" VerticalContentAlignment="Center" Watermark="{x:Static resx:ResUI.MsgServerTitle}" /> + + + + + + - + + - - + - - - - - - - - - - - - - - + @@ -139,6 +137,7 @@ + @@ -146,6 +145,16 @@ + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs index 5bade399..418d6d20 100644 --- a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs @@ -1,14 +1,4 @@ -using System.Reactive.Disposables; -using System.Reactive.Linq; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.ReactiveUI; -using Avalonia.Threading; using DialogHostAvalonia; -using MsBox.Avalonia.Enums; -using ReactiveUI; -using Splat; using v2rayN.Desktop.Common; namespace v2rayN.Desktop.Views; @@ -48,7 +38,6 @@ public partial class ProfilesView : ReactiveUserControl //} ViewModel = new ProfilesViewModel(UpdateViewHandler); - Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(ProfilesViewModel)); this.WhenActivated(disposables => { @@ -68,11 +57,12 @@ public partial class ProfilesView : ReactiveUserControl this.BindCommand(ViewModel, vm => vm.CopyServerCmd, v => v.menuCopyServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayRandomCmd, v => v.menuSetDefaultMultipleServerXrayRandom).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayRoundRobinCmd, v => v.menuSetDefaultMultipleServerXrayRoundRobin).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayLeastPingCmd, v => v.menuSetDefaultMultipleServerXrayLeastPing).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayLeastLoadCmd, v => v.menuSetDefaultMultipleServerXrayLeastLoad).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerSingBoxLeastPingCmd, v => v.menuSetDefaultMultipleServerSingBoxLeastPing).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRandomCmd, v => v.menuGenGroupMultipleServerXrayRandom).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRoundRobinCmd, v => v.menuGenGroupMultipleServerXrayRoundRobin).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastPingCmd, v => v.menuGenGroupMultipleServerXrayLeastPing).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastLoadCmd, v => v.menuGenGroupMultipleServerXrayLeastLoad).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxLeastPingCmd, v => v.menuGenGroupMultipleServerSingBoxLeastPing).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxFallbackCmd, v => v.menuGenGroupMultipleServerSingBoxFallback).DisposeWith(disposables); //servers move //this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables); @@ -90,6 +80,7 @@ public partial class ProfilesView : ReactiveUserControl this.BindCommand(ViewModel, vm => vm.SpeedServerCmd, v => v.menuSpeedServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SortServerResultCmd, v => v.menuSortServerResult).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.RemoveInvalidServerResultCmd, v => v.menuRemoveInvalidServerResult).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.FastRealPingCmd, v => v.btnFastRealPing).DisposeWith(disposables); //servers export this.BindCommand(ViewModel, vm => vm.Export2ClientConfigCmd, v => v.menuExport2ClientConfig).DisposeWith(disposables); @@ -133,7 +124,10 @@ public partial class ProfilesView : ReactiveUserControl { case EViewAction.SetClipboardData: if (obj is null) + { return false; + } + await AvaUtils.SetClipboardData(this, (string)obj); break; @@ -150,7 +144,10 @@ public partial class ProfilesView : ReactiveUserControl case EViewAction.SaveFileDialog: if (obj is null) + { return false; + } + var fileName = await UI.SaveFileDialog(_window, ""); if (fileName.IsNullOrEmpty()) { @@ -161,23 +158,43 @@ public partial class ProfilesView : ReactiveUserControl case EViewAction.AddServerWindow: if (obj is null) + { return false; + } + return await new AddServerWindow((ProfileItem)obj).ShowDialog(_window); case EViewAction.AddServer2Window: if (obj is null) + { return false; + } + return await new AddServer2Window((ProfileItem)obj).ShowDialog(_window); + case EViewAction.AddGroupServerWindow: + if (obj is null) + { + return false; + } + + return await new AddGroupServerWindow((ProfileItem)obj).ShowDialog(_window); + case EViewAction.ShareServer: if (obj is null) + { return false; + } + await ShareServer((string)obj); break; case EViewAction.SubEditWindow: if (obj is null) + { return false; + } + return await new SubEditWindow((SubItem)obj).ShowDialog(_window); case EViewAction.DispatcherRefreshServersBiz: @@ -219,14 +236,17 @@ public partial class ProfilesView : ReactiveUserControl { var source = e.Source as Border; if (source?.Name == "HeaderBackground") + { return; + } + if (_config.UiItem.DoubleClick2Activate) { ViewModel?.SetDefaultServer(); } else { - ViewModel?.EditServerAsync(EConfigType.Custom); + ViewModel?.EditServerAsync(); } } @@ -267,7 +287,7 @@ public partial class ProfilesView : ReactiveUserControl break; case Key.D: - ViewModel?.EditServerAsync(EConfigType.Custom); + ViewModel?.EditServerAsync(); break; case Key.F: diff --git a/v2rayN/v2rayN.Desktop/Views/QrcodeView.axaml b/v2rayN/v2rayN.Desktop/Views/QrcodeView.axaml index 90d94685..f9c8821d 100644 --- a/v2rayN/v2rayN.Desktop/Views/QrcodeView.axaml +++ b/v2rayN/v2rayN.Desktop/Views/QrcodeView.axaml @@ -4,19 +4,25 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - d:DesignHeight="480" - d:DesignWidth="400" + xmlns:sys="clr-namespace:System;assembly=netstandard" + d:DesignHeight="600" + d:DesignWidth="600" mc:Ignorable="d"> - + + + 400 + + + + Width="{StaticResource QrcodeWidth}" + Height="{StaticResource QrcodeWidth}" /> + RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto"> - + VerticalAlignment="Center" + Orientation="Horizontal"> + + + - + - + - - + + + + + + + Header="{x:Static resx:ResUI.menuCopyServer}" /> - + Header="{x:Static resx:ResUI.menuRemoveInvalidServerResult}" /> - - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/v2rayN/v2rayN/Views/ProfilesView.xaml.cs b/v2rayN/v2rayN/Views/ProfilesView.xaml.cs index 8ef236ab..7dae358c 100644 --- a/v2rayN/v2rayN/Views/ProfilesView.xaml.cs +++ b/v2rayN/v2rayN/Views/ProfilesView.xaml.cs @@ -1,14 +1,7 @@ -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; -using System.Windows.Input; using System.Windows.Media; -using System.Windows.Threading; using MaterialDesignThemes.Wpf; -using ReactiveUI; -using Splat; using v2rayN.Base; using Point = System.Windows.Point; @@ -42,7 +35,6 @@ public partial class ProfilesView } ViewModel = new ProfilesViewModel(UpdateViewHandler); - Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(ProfilesViewModel)); this.WhenActivated(disposables => { @@ -62,11 +54,12 @@ public partial class ProfilesView this.BindCommand(ViewModel, vm => vm.CopyServerCmd, v => v.menuCopyServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayRandomCmd, v => v.menuSetDefaultMultipleServerXrayRandom).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayRoundRobinCmd, v => v.menuSetDefaultMultipleServerXrayRoundRobin).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayLeastPingCmd, v => v.menuSetDefaultMultipleServerXrayLeastPing).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerXrayLeastLoadCmd, v => v.menuSetDefaultMultipleServerXrayLeastLoad).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.SetDefaultMultipleServerSingBoxLeastPingCmd, v => v.menuSetDefaultMultipleServerSingBoxLeastPing).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRandomCmd, v => v.menuGenGroupMultipleServerXrayRandom).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRoundRobinCmd, v => v.menuGenGroupMultipleServerXrayRoundRobin).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastPingCmd, v => v.menuGenGroupMultipleServerXrayLeastPing).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastLoadCmd, v => v.menuGenGroupMultipleServerXrayLeastLoad).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxLeastPingCmd, v => v.menuGenGroupMultipleServerSingBoxLeastPing).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxFallbackCmd, v => v.menuGenGroupMultipleServerSingBoxFallback).DisposeWith(disposables); //servers move this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables); @@ -84,6 +77,7 @@ public partial class ProfilesView this.BindCommand(ViewModel, vm => vm.SpeedServerCmd, v => v.menuSpeedServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SortServerResultCmd, v => v.menuSortServerResult).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.RemoveInvalidServerResultCmd, v => v.menuRemoveInvalidServerResult).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.FastRealPingCmd, v => v.btnFastRealPing).DisposeWith(disposables); //servers export this.BindCommand(ViewModel, vm => vm.Export2ClientConfigCmd, v => v.menuExport2ClientConfig).DisposeWith(disposables); @@ -115,7 +109,10 @@ public partial class ProfilesView { case EViewAction.SetClipboardData: if (obj is null) + { return false; + } + WindowsUtils.SetClipboardData((string)obj); break; @@ -132,8 +129,11 @@ public partial class ProfilesView case EViewAction.SaveFileDialog: if (obj is null) + { return false; - if (UI.SaveFileDialog(out string fileName, "Config|*.json") != true) + } + + if (UI.SaveFileDialog(out var fileName, "Config|*.json") != true) { return false; } @@ -142,24 +142,44 @@ public partial class ProfilesView case EViewAction.AddServerWindow: if (obj is null) + { return false; - return (new AddServerWindow((ProfileItem)obj)).ShowDialog() ?? false; + } + + return new AddServerWindow((ProfileItem)obj).ShowDialog() ?? false; case EViewAction.AddServer2Window: if (obj is null) + { return false; - return (new AddServer2Window((ProfileItem)obj)).ShowDialog() ?? false; + } + + return new AddServer2Window((ProfileItem)obj).ShowDialog() ?? false; + + case EViewAction.AddGroupServerWindow: + if (obj is null) + { + return false; + } + + return new AddGroupServerWindow((ProfileItem)obj).ShowDialog() ?? false; case EViewAction.ShareServer: if (obj is null) + { return false; + } + ShareServer((string)obj); break; case EViewAction.SubEditWindow: if (obj is null) + { return false; - return (new SubEditWindow((SubItem)obj)).ShowDialog() ?? false; + } + + return new SubEditWindow((SubItem)obj).ShowDialog() ?? false; case EViewAction.DispatcherRefreshServersBiz: Application.Current?.Dispatcher.Invoke(RefreshServersBiz, DispatcherPriority.Normal); @@ -171,7 +191,7 @@ public partial class ProfilesView public async void ShareServer(string url) { - var img = QRCodeUtils.GetQRCode(url); + var img = QRCodeWindowsUtils.GetQRCode(url); var dialog = new QrcodeView() { imgQrcode = { Source = img }, @@ -210,7 +230,7 @@ public partial class ProfilesView } else { - ViewModel?.EditServerAsync(EConfigType.Custom); + ViewModel?.EditServerAsync(); } } @@ -246,7 +266,7 @@ public partial class ProfilesView break; case Key.D: - ViewModel?.EditServerAsync(EConfigType.Custom); + ViewModel?.EditServerAsync(); break; case Key.F: @@ -416,8 +436,8 @@ public partial class ProfilesView private void LstProfiles_MouseMove(object sender, MouseEventArgs e) { // Get the current mouse position - Point mousePos = e.GetPosition(null); - Vector diff = startPoint - mousePos; + var mousePos = e.GetPosition(null); + var diff = startPoint - mousePos; if (e.LeftButton == MouseButtonState.Pressed && (Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance || @@ -425,15 +445,22 @@ public partial class ProfilesView { // Get the dragged Item if (sender is not DataGrid listView) + { return; + } + var listViewItem = FindAncestor((DependencyObject)e.OriginalSource); if (listViewItem == null) + { return; // Abort - // Find the data behind the ListViewItem - ProfileItemModel item = (ProfileItemModel)listView.ItemContainerGenerator.ItemFromContainer(listViewItem); + } + // Find the data behind the ListViewItem + var item = (ProfileItemModel)listView.ItemContainerGenerator.ItemFromContainer(listViewItem); if (item == null) + { return; // Abort - // Initialize the drag & drop operation + } + // Initialize the drag & drop operation startIndex = lstProfiles.SelectedIndex; DataObject dragData = new(formatData, item); DragDrop.DoDragDrop(listViewItem, dragData, DragDropEffects.Copy | DragDropEffects.Move); @@ -454,7 +481,10 @@ public partial class ProfilesView { // Get the drop Item destination if (sender is not DataGrid listView) + { return; + } + var listViewItem = FindAncestor((DependencyObject)e.OriginalSource); if (listViewItem == null) { @@ -463,9 +493,11 @@ public partial class ProfilesView return; } // Find the data behind the Item - ProfileItemModel item = (ProfileItemModel)listView.ItemContainerGenerator.ItemFromContainer(listViewItem); + var item = (ProfileItemModel)listView.ItemContainerGenerator.ItemFromContainer(listViewItem); if (item == null) + { return; + } // Move item into observable collection // (this will be automatically reflected to lstView.ItemsSource) e.Effects = DragDropEffects.Move; diff --git a/v2rayN/v2rayN/Views/QrcodeView.xaml b/v2rayN/v2rayN/Views/QrcodeView.xaml index d6c59457..b6e88273 100644 --- a/v2rayN/v2rayN/Views/QrcodeView.xaml +++ b/v2rayN/v2rayN/Views/QrcodeView.xaml @@ -1,4 +1,4 @@ - - - - - - - + + 400 + - + + +