From df016dd55c3816fc6d8566a7cec51db3258ee69c Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Fri, 30 Jan 2026 10:35:00 +0800 Subject: [PATCH 01/24] Bug fix https://github.com/2dust/v2rayN/issues/8720 --- v2rayN/ServiceLib/Common/Extension.cs | 24 +++++++++++++++++++ .../Manager/ActionPrecheckManager.cs | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/v2rayN/ServiceLib/Common/Extension.cs b/v2rayN/ServiceLib/Common/Extension.cs index b6ee8154..1403d127 100644 --- a/v2rayN/ServiceLib/Common/Extension.cs +++ b/v2rayN/ServiceLib/Common/Extension.cs @@ -94,4 +94,28 @@ public static class Extension { return configType is EConfigType.Custom or EConfigType.PolicyGroup or EConfigType.ProxyChain; } + + /// + /// Safely adds elements from a collection to the list. Does nothing if the source is null. + /// + public static void AddRangeSafe(this ICollection destination, IEnumerable? source) + { + ArgumentNullException.ThrowIfNull(destination); + + if (source is null) + { + return; + } + + if (destination is List list) + { + list.AddRange(source); + return; + } + + foreach (var item in source) + { + destination.Add(item); + } + } } diff --git a/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs b/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs index 74e5f5ef..965335cc 100644 --- a/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs +++ b/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs @@ -218,8 +218,8 @@ public class ActionPrecheckManager var childIds = new List(); var subItems = await ProfileGroupItemManager.GetSubChildProfileItems(group); - childIds.AddRange(subItems.Select(p => p.IndexId)); - childIds.AddRange(Utils.String2List(group.ChildItems)); + childIds.AddRangeSafe(subItems.Select(p => p.IndexId)); + childIds.AddRangeSafe(Utils.String2List(group.ChildItems)); foreach (var child in childIds) { From 8774e302b23c5fe000ed1d69fe845e0c6be08113 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Fri, 30 Jan 2026 10:43:01 +0800 Subject: [PATCH 02/24] up 7.17.2 --- v2rayN/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2rayN/Directory.Build.props b/v2rayN/Directory.Build.props index be0a6e64..73d0b7e7 100644 --- a/v2rayN/Directory.Build.props +++ b/v2rayN/Directory.Build.props @@ -1,7 +1,7 @@ - 7.17.1 + 7.17.2 From ffe401a26d2bdc9b906d48b7856011c315b1b4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E8=90=BD?= <53731501+qyl27@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:32:06 +0800 Subject: [PATCH 03/24] Accept hosts.ics as a host file on windows. (#8714) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 秋雨落 --- v2rayN/ServiceLib/Common/Utils.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index fe2b1549..0f4feda5 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -719,10 +719,9 @@ public class Utils return Guid.TryParse(strSrc, out _); } - public static Dictionary GetSystemHosts() + private static Dictionary GetSystemHosts(string hostFile) { var systemHosts = new Dictionary(); - var hostFile = @"C:\Windows\System32\drivers\etc\hosts"; try { if (File.Exists(hostFile)) @@ -755,6 +754,19 @@ public class Utils return systemHosts; } + public static Dictionary GetSystemHosts() + { + var hosts = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts"); + var hostsIcs = GetSystemHosts(@"C:\Windows\System32\drivers\etc\hosts.ics"); + + foreach (var (key, value) in hostsIcs) + { + hosts[key] = value; + } + + return hosts; + } + public static async Task GetCliWrapOutput(string filePath, string? arg) { return await GetCliWrapOutput(filePath, arg != null ? new List() { arg } : null); From 4550ddb14e2252a772972a8262e879be60ca6020 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Sat, 31 Jan 2026 10:34:25 +0800 Subject: [PATCH 04/24] Bug fix (#8728) --- .../Services/CoreConfig/Singbox/SingboxRoutingService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index 58bcaf99..d53f29f8 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -281,7 +281,9 @@ public partial class CoreConfigSingboxService if (_config.TunModeItem.EnableTun && item.Process?.Count > 0) { var ruleProcName = JsonUtils.DeepCopy(rule3); + ruleProcName.process_name ??= []; var ruleProcPath = JsonUtils.DeepCopy(rule3); + ruleProcPath.process_path ??= []; foreach (var process in item.Process) { // sing-box doesn't support this, fall back to process name match From d589713fd5fcef15ff34179f20a1dc8f922ef2a3 Mon Sep 17 00:00:00 2001 From: JieXu Date: Sat, 31 Jan 2026 11:40:21 +0800 Subject: [PATCH 05/24] Update build-linux.yml (#8724) --- .github/workflows/build-linux.yml | 61 +++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 7ac333f2..4c7b9ff1 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -103,11 +103,64 @@ jobs: steps: - name: Prepare tools (Red Hat) + shell: bash run: | - dnf repolist all - dnf -y makecache - dnf -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm - dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which + set -euo pipefail + + . /etc/os-release + EL_MAJOR="${VERSION_ID%%.*}" + echo "EL_MAJOR=${EL_MAJOR}" + + dnf -y makecache || true + command -v curl >/dev/null || dnf -y install curl ca-certificates + + ARCH="$(uname -m)" + case "$ARCH" in x86_64|aarch64) ;; *) echo "Unsupported arch: $ARCH"; exit 1 ;; esac + + install_epel_from_dir() { + local base="$1" rpm + echo "Try: $base" + + rpm="$( + { + curl -fsSL "$base/Packages/" 2>/dev/null + curl -fsSL "$base/Packages/e/" 2>/dev/null | sed 's|href="|href="e/|' + } | + sed -n 's/.*href="\([^"]*epel-release-[^"]*\.noarch\.rpm\)".*/\1/p' | + sort -V | tail -n1 + )" || true + + if [[ -n "$rpm" ]]; then + dnf -y install "$base/Packages/$rpm" + return 0 + fi + return 1 + } + + FEDORA="https://dl.fedoraproject.org/pub/epel/epel-release-latest-${EL_MAJOR}.noarch.rpm" + echo "Try Fedora: $FEDORA" + + if curl -fsSLI "$FEDORA" >/dev/null; then + dnf -y install "$FEDORA" + else + ROCKY="https://dl.rockylinux.org/pub/rocky/${EL_MAJOR}/extras/${ARCH}/os" + if install_epel_from_dir "$ROCKY"; then + : + else + ALMA="https://repo.almalinux.org/almalinux/${EL_MAJOR}/extras/${ARCH}/os" + if install_epel_from_dir "$ALMA"; then + : + else + echo "EPEL bootstrap failed (Fedora/Rocky/Alma)" + exit 1 + fi + fi + fi + + dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core \ + rsync findutils tar gzip unzip which + + dnf repolist | grep -i epel || true - name: Checkout repo (for scripts) uses: actions/checkout@v6.0.1 From eb0f5bafde0dc2ea06144f7d5e97dcc4ebe71508 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Sun, 1 Feb 2026 15:34:04 +0800 Subject: [PATCH 06/24] Disable insecure when cert pinned (#8733) --- .../ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index 3db5a420..1f5ad4ef 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -325,6 +325,7 @@ public partial class CoreConfigV2rayService else if (!node.CertSha.IsNullOrEmpty()) { tlsSettings.pinnedPeerCertSha256 = node.CertSha; + tlsSettings.allowInsecure = false; } streamSettings.tlsSettings = tlsSettings; } From 585c24526fd8b537eadf86ac89e42a9f08f736eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 15:35:19 +0800 Subject: [PATCH 07/24] Bump actions/checkout from 6.0.1 to 6.0.2 (#8694) Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.1 to 6.0.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6.0.1...v6.0.2) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-linux.yml | 4 ++-- .github/workflows/build-osx.yml | 2 +- .github/workflows/build-windows-desktop.yml | 2 +- .github/workflows/build-windows.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 4c7b9ff1..394d2862 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6.0.1 + uses: actions/checkout@v6.0.2 with: submodules: 'recursive' fetch-depth: '0' @@ -163,7 +163,7 @@ jobs: dnf repolist | grep -i epel || true - name: Checkout repo (for scripts) - uses: actions/checkout@v6.0.1 + uses: actions/checkout@v6.0.2 with: submodules: 'recursive' fetch-depth: '0' diff --git a/.github/workflows/build-osx.yml b/.github/workflows/build-osx.yml index ee01e92c..f36b2b0e 100644 --- a/.github/workflows/build-osx.yml +++ b/.github/workflows/build-osx.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6.0.1 + uses: actions/checkout@v6.0.2 with: submodules: 'recursive' fetch-depth: '0' diff --git a/.github/workflows/build-windows-desktop.yml b/.github/workflows/build-windows-desktop.yml index b2c3f794..16f3771f 100644 --- a/.github/workflows/build-windows-desktop.yml +++ b/.github/workflows/build-windows-desktop.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6.0.1 + uses: actions/checkout@v6.0.2 with: submodules: 'recursive' fetch-depth: '0' diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index e4603154..9624ba43 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6.0.1 + uses: actions/checkout@v6.0.2 - name: Setup uses: actions/setup-dotnet@v5.0.1 From 7678ad90950be3b6b2976dba4cf2d349bf644f76 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Sun, 1 Feb 2026 15:49:00 +0800 Subject: [PATCH 08/24] up 7.17.3 --- v2rayN/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2rayN/Directory.Build.props b/v2rayN/Directory.Build.props index 73d0b7e7..d44c63bd 100644 --- a/v2rayN/Directory.Build.props +++ b/v2rayN/Directory.Build.props @@ -1,7 +1,7 @@ - 7.17.2 + 7.17.3 From 19d4f1fa831862a5cff3697909a92a42094f3e7a Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:15:36 +0800 Subject: [PATCH 09/24] Enable sudo for mihomo core in TUN mode https://github.com/2dust/v2rayN/issues/8673 --- v2rayN/ServiceLib/Manager/CoreManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2rayN/ServiceLib/Manager/CoreManager.cs b/v2rayN/ServiceLib/Manager/CoreManager.cs index 79dbdeb0..53240960 100644 --- a/v2rayN/ServiceLib/Manager/CoreManager.cs +++ b/v2rayN/ServiceLib/Manager/CoreManager.cs @@ -226,7 +226,7 @@ public class CoreManager { if (mayNeedSudo && _config.TunModeItem.EnableTun - && coreInfo.CoreType == ECoreType.sing_box + && (coreInfo.CoreType is ECoreType.sing_box or ECoreType.mihomo) && Utils.IsNonWindows()) { _linuxSudo = true; From c7afef3d70d292bb3a45f0e1f236242935693a89 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:15:40 +0800 Subject: [PATCH 10/24] Update Directory.Packages.props --- v2rayN/Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v2rayN/Directory.Packages.props b/v2rayN/Directory.Packages.props index 54567a21..51bdd56f 100644 --- a/v2rayN/Directory.Packages.props +++ b/v2rayN/Directory.Packages.props @@ -5,7 +5,7 @@ false - + @@ -22,7 +22,7 @@ - + From fdde837698d53f81696daf24954fc8b107244c2c Mon Sep 17 00:00:00 2001 From: DHR60 Date: Wed, 4 Feb 2026 02:34:07 +0000 Subject: [PATCH 11/24] Add xray v26.1.31 finalmask support (#8732) * Add xray v26.1.31 hysteria2 support * Add xray v26.1.31 mkcp support --- v2rayN/ServiceLib/Global.cs | 10 +++++ v2rayN/ServiceLib/Models/V2rayConfig.cs | 23 +++++----- .../CoreConfig/V2ray/V2rayOutboundService.cs | 42 +++++++++++++++---- 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index 94a66fa5..6c22a045 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -288,6 +288,16 @@ public class Global "dns" ]; + public static readonly Dictionary KcpHeaderMaskMap = new() + { + { "srtp", "header-srtp" }, + { "utp", "header-utp" }, + { "wechat-video", "header-wechat" }, + { "dtls", "header-dtls" }, + { "wireguard", "header-wireguard" }, + { "dns", "header-dns" } + }; + public static readonly List CoreTypes = [ "Xray", diff --git a/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayN/ServiceLib/Models/V2rayConfig.cs index d9575432..acf17532 100644 --- a/v2rayN/ServiceLib/Models/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/V2rayConfig.cs @@ -343,7 +343,7 @@ public class StreamSettings4Ray public HysteriaSettings4Ray? hysteriaSettings { get; set; } - public List? udpmasks { get; set; } + public FinalMask4Ray? finalmask { get; set; } public Sockopt4Ray? sockopt { get; set; } } @@ -388,8 +388,6 @@ public class Header4Ray public object request { get; set; } public object response { get; set; } - - public string? domain { get; set; } } public class KcpSettings4Ray @@ -407,10 +405,6 @@ public class KcpSettings4Ray public int readBufferSize { get; set; } public int writeBufferSize { get; set; } - - public Header4Ray header { get; set; } - - public string seed { get; set; } } public class WsSettings4Ray @@ -484,15 +478,22 @@ public class HysteriaUdpHop4Ray public int? interval { get; set; } } -public class UdpMasks4Ray +public class FinalMask4Ray { - public string type { get; set; } - public UdpMasksSettings4Ray? settings { get; set; } + public List? tcp { get; set; } + public List? udp { get; set; } } -public class UdpMasksSettings4Ray +public class Mask4Ray +{ + public string type { get; set; } + public MaskSettings4Ray? settings { get; set; } +} + +public class MaskSettings4Ray { public string? password { get; set; } + public string? domain { get; set; } } public class AccountsItem4Ray diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index 1f5ad4ef..d3575685 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -365,14 +365,33 @@ public partial class CoreConfigV2rayService kcpSettings.congestion = _config.KcpItem.Congestion; kcpSettings.readBufferSize = _config.KcpItem.ReadBufferSize; kcpSettings.writeBufferSize = _config.KcpItem.WriteBufferSize; - kcpSettings.header = new Header4Ray + streamSettings.finalmask ??= new(); + if (Global.KcpHeaderMaskMap.TryGetValue(node.HeaderType, out var header)) { - type = node.HeaderType, - domain = host.NullIfEmpty() - }; - if (path.IsNotEmpty()) + streamSettings.finalmask.udp = + [ + new Mask4Ray + { + type = header, + settings = node.HeaderType == "dns" && !host.IsNullOrEmpty() ? new MaskSettings4Ray { domain = host } : null + } + ]; + } + streamSettings.finalmask.udp ??= []; + if (path.IsNullOrEmpty()) { - kcpSettings.seed = path; + streamSettings.finalmask.udp.Add(new Mask4Ray + { + type = "mkcp-original" + }); + } + else + { + streamSettings.finalmask.udp.Add(new Mask4Ray + { + type = "mkcp-aes128gcm", + settings = new MaskSettings4Ray { password = path } + }); } streamSettings.kcpSettings = kcpSettings; break; @@ -513,8 +532,15 @@ public partial class CoreConfigV2rayService streamSettings.hysteriaSettings = hysteriaSettings; if (node.Path.IsNotEmpty()) { - streamSettings.udpmasks = - [new() { type = "salamander", settings = new() { password = node.Path.TrimEx(), } }]; + streamSettings.finalmask ??= new(); + streamSettings.finalmask.udp = + [ + new Mask4Ray + { + type = "salamander", + settings = new MaskSettings4Ray { password = node.Path.TrimEx(), } + } + ]; } break; From 3cb640c16b9ccfb583473504c346c7c66d58be2e Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:33:34 +0800 Subject: [PATCH 12/24] Add IP validation and improve hosts parsing https://github.com/2dust/v2rayN/issues/8752 --- v2rayN/ServiceLib/Common/Utils.cs | 96 +++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 19 deletions(-) diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index 0f4feda5..9ce27fa3 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -505,6 +505,31 @@ public class Utils return false; } + public static bool IsIpAddress(string? ip) + { + if (ip.IsNullOrEmpty()) + { + return false; + } + + ip = ip.Trim(); + + // First, validate using built-in parser + if (!IPAddress.TryParse(ip, out var address)) + { + return false; + } + + // For IPv4: ensure it has exactly 3 dots (meaning 4 parts) + if (address.AddressFamily == AddressFamily.InterNetwork) + { + return ip.Count(c => c == '.') == 3; + } + + // For IPv6: TryParse is already strict enough + return address.AddressFamily == AddressFamily.InterNetworkV6; + } + public static Uri? TryUri(string url) { try @@ -724,27 +749,60 @@ public class Utils var systemHosts = new Dictionary(); try { - if (File.Exists(hostFile)) + if (!File.Exists(hostFile)) { - var hosts = File.ReadAllText(hostFile).Replace("\r", ""); - var hostsList = hosts.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); - - 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]); - } + return systemHosts; } + var hosts = File.ReadAllText(hostFile).Replace("\r", ""); + var hostsList = hosts.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var host in hostsList) + { + // Trim whitespace + var line = host.Trim(); + + // Skip comments and empty lines + if (line.IsNullOrEmpty() || line.StartsWith("#")) + { + continue; + } + + // Strip inline comments + var commentIndex = line.IndexOf('#'); + if (commentIndex >= 0) + { + line = line.Substring(0, commentIndex).Trim(); + } + if (line.IsNullOrEmpty()) + { + continue; + } + + var hostItem = line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + if (hostItem.Length < 2) + { + continue; + } + + var ipAddress = hostItem[0]; + var domain = hostItem[1]; + + // Validate IP address + if (!IsIpAddress(ipAddress)) + { + continue; + } + + // Validate domain name + if (domain.IsNullOrEmpty() || domain.Length > 255) + { + continue; + } + + systemHosts[domain] = ipAddress; + } + + return systemHosts; } catch (Exception ex) { From 7e2e66bb0e21066ecdaad0f1276cc58dc6046c26 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Wed, 4 Feb 2026 06:35:26 +0000 Subject: [PATCH 13/24] Add DNS features (#8729) * Simplify DNS Settings * Add ParallelQuery and ServeStale features * Fix * Add Tips * Simplify Predefined Hosts --- v2rayN/ServiceLib/Common/Utils.cs | 12 ++ v2rayN/ServiceLib/Global.cs | 53 +++--- v2rayN/ServiceLib/Handler/ConfigHandler.cs | 2 + v2rayN/ServiceLib/Models/ConfigItems.cs | 7 +- v2rayN/ServiceLib/Models/V2rayConfig.cs | 12 +- v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 94 +++++++---- v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 34 ++-- v2rayN/ServiceLib/Resx/ResUI.fr.resx | 34 ++-- v2rayN/ServiceLib/Resx/ResUI.hu.resx | 34 ++-- v2rayN/ServiceLib/Resx/ResUI.resx | 34 ++-- v2rayN/ServiceLib/Resx/ResUI.ru.resx | 34 ++-- v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 34 ++-- v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 34 ++-- .../CoreConfig/Singbox/SingboxDnsService.cs | 10 +- .../Singbox/SingboxRoutingService.cs | 19 ++- .../CoreConfig/V2ray/V2rayDnsService.cs | 153 ++++++++++-------- .../ViewModels/DNSSettingViewModel.cs | 21 +-- .../Views/DNSSettingWindow.axaml | 115 +++++++++---- .../Views/DNSSettingWindow.axaml.cs | 16 +- .../Views/RoutingRuleSettingWindow.axaml.cs | 2 +- .../Views/RoutingSettingWindow.axaml.cs | 2 +- v2rayN/v2rayN/Views/DNSSettingWindow.xaml | 97 +++++++---- v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs | 16 +- .../Views/RoutingRuleSettingWindow.xaml.cs | 2 +- .../v2rayN/Views/RoutingSettingWindow.xaml.cs | 2 +- 25 files changed, 554 insertions(+), 319 deletions(-) diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index 9ce27fa3..7824ff4a 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -462,6 +462,18 @@ public class Utils return (domain, port); } + public static string? DomainStrategy4Sbox(string? strategy) + { + return strategy switch + { + not null when strategy.StartsWith("UseIPv4") => "prefer_ipv4", + not null when strategy.StartsWith("UseIPv6") => "prefer_ipv6", + not null when strategy.StartsWith("ForceIPv4") => "ipv4_only", + not null when strategy.StartsWith("ForceIPv6") => "ipv6_only", + _ => null + }; + } + #endregion Conversion Functions #region Data Checks diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index 6c22a045..486e53ed 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -339,13 +339,13 @@ public class Global IPOnDemand ]; - public static readonly List DomainStrategies4Singbox = + public static readonly List DomainStrategies4Sbox = [ - "ipv4_only", - "ipv6_only", + "", "prefer_ipv4", "prefer_ipv6", - "" + "ipv4_only", + "ipv6_only" ]; public static readonly List Fingerprints = @@ -387,28 +387,22 @@ public class Global "" ]; - public static readonly List DomainStrategy4Freedoms = + public static readonly List DomainStrategy = [ "AsIs", "UseIP", + "UseIPv4v6", + "UseIPv6v4", "UseIPv4", "UseIPv6", "" ]; - public static readonly List SingboxDomainStrategy4Out = - [ - "", - "ipv4_only", - "prefer_ipv4", - "prefer_ipv6", - "ipv6_only" - ]; - public static readonly List DomainDirectDNSAddress = [ "https://dns.alidns.com/dns-query", "https://doh.pub/dns-query", + "https://dns.alidns.com/dns-query,https://doh.pub/dns-query", "223.5.5.5", "119.29.29.29", "localhost" @@ -417,8 +411,9 @@ public class Global public static readonly List DomainRemoteDNSAddress = [ "https://cloudflare-dns.com/dns-query", - "https://dns.cloudflare.com/dns-query", "https://dns.google/dns-query", + "https://cloudflare-dns.com/dns-query,https://dns.google/dns-query,8.8.8.8", + "https://dns.cloudflare.com/dns-query", "https://doh.dns.sb/dns-query", "https://doh.opendns.com/dns-query", "https://common.dot.dns.yandex.net", @@ -616,20 +611,20 @@ public class Global public static readonly Dictionary> PredefinedHosts = new() { - { "dns.google", new List { "8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844" } }, - { "dns.alidns.com", new List { "223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1" } }, - { "one.one.one.one", new List { "1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001" } }, - { "1dot1dot1dot1.cloudflare-dns.com", new List { "1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001" } }, - { "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::" } }, - { "dns.umbrella.com", new List { "208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53" } }, - { "dns.sse.cisco.com", new List { "208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53" } }, - { "engage.cloudflareclient.com", new List { "162.159.192.1", "2606:4700:d0::a29f:c001" } } + { "dns.google", ["8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844"] }, + { "dns.alidns.com", ["223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1"] }, + { "one.one.one.one", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] }, + { "1dot1dot1dot1.cloudflare-dns.com", ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] }, + { "cloudflare-dns.com", ["104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9"] }, + { "dns.cloudflare.com", ["104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5"] }, + { "dot.pub", ["1.12.12.12", "120.53.53.53"] }, + { "doh.pub", ["1.12.12.12", "120.53.53.53"] }, + { "dns.quad9.net", ["9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9"] }, + { "dns.yandex.net", ["77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff"] }, + { "dns.sb", ["185.222.222.222", "2a09::"] }, + { "dns.umbrella.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] }, + { "dns.sse.cisco.com", ["208.67.220.220", "208.67.222.222", "2620:119:35::35", "2620:119:53::53"] }, + { "engage.cloudflareclient.com", ["162.159.192.1"] } }; public static readonly List ExpectedIPs = diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index 5877d012..f9ff4494 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -114,6 +114,8 @@ public static class ConfigHandler config.SimpleDNSItem ??= InitBuiltinSimpleDNS(); config.SimpleDNSItem.GlobalFakeIp ??= true; config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault(); + config.SimpleDNSItem.ServeStale ??= false; + config.SimpleDNSItem.ParallelQuery ??= false; config.SpeedTestItem ??= new(); if (config.SpeedTestItem.SpeedTestTimeout < 10) diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs index eeb88deb..46297e40 100644 --- a/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/ConfigItems.cs @@ -265,9 +265,10 @@ public class SimpleDNSItem public string? DirectDNS { get; set; } public string? RemoteDNS { get; set; } public string? BootstrapDNS { get; set; } - public string? RayStrategy4Freedom { get; set; } - public string? SingboxStrategy4Direct { get; set; } - public string? SingboxStrategy4Proxy { get; set; } + public string? Strategy4Freedom { get; set; } + public string? Strategy4Proxy { get; set; } + public bool? ServeStale { get; set; } + public bool? ParallelQuery { get; set; } public string? Hosts { get; set; } public string? DirectExpectedIPs { get; set; } } diff --git a/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayN/ServiceLib/Models/V2rayConfig.cs index acf17532..c752168a 100644 --- a/v2rayN/ServiceLib/Models/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/V2rayConfig.cs @@ -3,7 +3,7 @@ namespace ServiceLib.Models; public class V2rayConfig { public Log4Ray log { get; set; } - public Dns4Ray dns { get; set; } + public object dns { get; set; } public List inbounds { get; set; } public List outbounds { get; set; } public Routing4Ray routing { get; set; } @@ -105,6 +105,8 @@ public class Outbounds4Ray public string protocol { get; set; } + public string? targetStrategy { get; set; } + public Outboundsettings4Ray settings { get; set; } public StreamSettings4Ray streamSettings { get; set; } @@ -206,12 +208,8 @@ public class Dns4Ray { public Dictionary? hosts { get; set; } public List servers { get; set; } - public string? clientIp { get; set; } - public string? queryStrategy { get; set; } - public bool? disableCache { get; set; } - public bool? disableFallback { get; set; } - public bool? disableFallbackIfMatch { get; set; } - public bool? useSystemHosts { get; set; } + public bool? serveStale { get; set; } + public bool? enableParallelQuery { get; set; } public string? tag { get; set; } } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index bf30c985..fd3b2d9f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -2727,6 +2727,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Direct Target Resolution Strategy 的本地化字符串。 + /// + public static string TbDirectResolveStrategy { + get { + return ResourceManager.GetString("TbDirectResolveStrategy", resourceCulture); + } + } + + /// + /// 查找类似 If unset or "AsIs", DNS resolution uses the system DNS; otherwise, the internal DNS module is used. 的本地化字符串。 + /// + public static string TbDirectResolveStrategyTips { + get { + return ResourceManager.GetString("TbDirectResolveStrategyTips", resourceCulture); + } + } + /// /// 查找类似 Display GUI 的本地化字符串。 /// @@ -2799,6 +2817,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 By default, invoked only during routing for resolution 的本地化字符串。 + /// + public static string TbDomesticDNSTips { + get { + return ResourceManager.GetString("TbDomesticDNSTips", resourceCulture); + } + } + /// /// 查找类似 EchConfigList 的本地化字符串。 /// @@ -3060,6 +3087,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Parallel Query 的本地化字符串。 + /// + public static string TbParallelQuery { + get { + return ResourceManager.GetString("TbParallelQuery", resourceCulture); + } + } + /// /// 查找类似 Path 的本地化字符串。 /// @@ -3214,7 +3250,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Via proxy — please ensure remote availability 的本地化字符串。 + /// 查找类似 By default, invoked only during routing for resolution; ensure the remote server can reach this DNS 的本地化字符串。 /// public static string TbRemoteDNSTips { get { @@ -3222,6 +3258,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Proxy Target Resolution Strategy 的本地化字符串。 + /// + public static string TbRemoteResolveStrategy { + get { + return ResourceManager.GetString("TbRemoteResolveStrategy", resourceCulture); + } + } + + /// + /// 查找类似 If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. 的本地化字符串。 + /// + public static string TbRemoteResolveStrategyTips { + get { + return ResourceManager.GetString("TbRemoteResolveStrategyTips", resourceCulture); + } + } + /// /// 查找类似 Camouflage domain(host) 的本地化字符串。 /// @@ -3357,15 +3411,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 sing-box Direct Resolution Strategy 的本地化字符串。 - /// - public static string TbSBDirectResolveStrategy { - get { - return ResourceManager.GetString("TbSBDirectResolveStrategy", resourceCulture); - } - } - /// /// 查找类似 sing-box Full Config Template 的本地化字符串。 /// @@ -3384,15 +3429,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 sing-box Remote Resolution Strategy 的本地化字符串。 - /// - public static string TbSBRemoteResolveStrategy { - get { - return ResourceManager.GetString("TbSBRemoteResolveStrategy", resourceCulture); - } - } - /// /// 查找类似 Encryption method (security) 的本地化字符串。 /// @@ -3438,6 +3474,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Serve Stale 的本地化字符串。 + /// + public static string TbServeStale { + get { + return ResourceManager.GetString("TbServeStale", resourceCulture); + } + } + /// /// 查找类似 Set system proxy 的本地化字符串。 /// @@ -4411,7 +4456,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs 的本地化字符串。 + /// 查找类似 When configured, validates IPs returned for regional domains (e.g., geosite:cn - geoip:cn), returning only expected IPs 的本地化字符串。 /// public static string TbValidateDirectExpectedIPsDesc { get { @@ -4419,15 +4464,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 xray Freedom Resolution Strategy 的本地化字符串。 - /// - public static string TbXrayFreedomStrategy { - get { - return ResourceManager.GetString("TbXrayFreedomStrategy", resourceCulture); - } - } - /// /// 查找类似 The delay: {0} ms, {1} 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 3a720cdd..16fe442f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1419,17 +1419,11 @@ Domestic DNS - - Via proxy — please ensure remote availability + + Direct Target Resolution Strategy - - xray Freedom Resolution Strategy - - - sing-box Direct Resolution Strategy - - - sing-box Remote Resolution Strategy + + Proxy Target Resolution Strategy Add Common DNS Hosts @@ -1453,7 +1447,7 @@ Validate Regional Domain IPs - When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs + When configured, validates IPs returned for regional domains (e.g., geosite:cn - geoip:cn), returning only expected IPs Enable Custom DNS @@ -1653,4 +1647,22 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Certificate fingerprint (SHA-256) + + Serve Stale + + + Parallel Query + + + By default, invoked only during routing for resolution + + + By default, invoked only during routing for resolution; ensure the remote server can reach this DNS + + + If unset or "AsIs", DNS resolution uses the system DNS; otherwise, the internal DNS module is used. + + + If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index f1710de6..a114ac21 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1416,17 +1416,11 @@ DNS direct - - Via le proxy ; assurez-vous que le serveur distant est disponible + + Direct Target Resolution Strategy - - Stratégie de résolution xray freedom - - - Stratégie de résolution directe sing-box - - - Stratégie de résolution distante sing-box + + Proxy Target Resolution Strategy Ajouter des hôtes DNS courants @@ -1450,7 +1444,7 @@ 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. + Après config, les IP renvoyées des domaines régionaux (ex. geosite:cn - geoip:cn) seront vérifiées ; seules les IP attendues seront retournées. Activer le DNS personnalisé @@ -1650,4 +1644,22 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Certificate fingerprint (SHA-256) + + Serve Stale + + + Parallel Query + + + By default, invoked only during routing for resolution + + + By default, invoked only during routing for resolution; ensure the remote server can reach this DNS + + + If unset or "AsIs", DNS resolution uses the system DNS; otherwise, the internal DNS module is used. + + + If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index 4787f0f1..21e4e68d 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1419,17 +1419,11 @@ Domestic DNS - - Via proxy — please ensure remote availability + + Direct Target Resolution Strategy - - xray Freedom Resolution Strategy - - - sing-box Direct Resolution Strategy - - - sing-box Remote Resolution Strategy + + Proxy Target Resolution Strategy Add Common DNS Hosts @@ -1453,7 +1447,7 @@ Validate Regional Domain IPs - When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs + When configured, validates IPs returned for regional domains (e.g., geosite:cn - geoip:cn), returning only expected IPs Enable Custom DNS @@ -1653,4 +1647,22 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Certificate fingerprint (SHA-256) + + Serve Stale + + + Parallel Query + + + By default, invoked only during routing for resolution + + + By default, invoked only during routing for resolution; ensure the remote server can reach this DNS + + + If unset or "AsIs", DNS resolution uses the system DNS; otherwise, the internal DNS module is used. + + + If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 6ad8a6e4..91095de4 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1419,17 +1419,11 @@ Domestic DNS - - Via proxy — please ensure remote availability + + Direct Target Resolution Strategy - - xray Freedom Resolution Strategy - - - sing-box Direct Resolution Strategy - - - sing-box Remote Resolution Strategy + + Proxy Target Resolution Strategy Add Common DNS Hosts @@ -1453,7 +1447,7 @@ Validate Regional Domain IPs - When configured, validates IPs returned for regional domains (e.g., geosite:cn), returning only expected IPs + When configured, validates IPs returned for regional domains (e.g., geosite:cn - geoip:cn), returning only expected IPs Enable Custom DNS @@ -1653,4 +1647,22 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Certificate fingerprint (SHA-256) + + Serve Stale + + + Parallel Query + + + By default, invoked only during routing for resolution + + + By default, invoked only during routing for resolution; ensure the remote server can reach this DNS + + + If unset or "AsIs", DNS resolution uses the system DNS; otherwise, the internal DNS module is used. + + + If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 3a0f9fef..adc9cc98 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1419,17 +1419,11 @@ Внутренний DNS - - Via proxy — please ensure remote availability + + Direct Target Resolution Strategy - - Стратегия резолвинга Freedom (Xray) - - - Стратегия прямого резолвинга (sing-box) - - - Стратегия удалённого резолвинга (sing-box) + + Proxy Target Resolution Strategy Добавить стандартные записи hosts (DNS) @@ -1453,7 +1447,7 @@ Проверять IP-адреса региональных доменов - При включении проверяет IP-адреса, возвращаемые для региональных доменов (например, geosite:cn), и оставляет только ожидаемые IP-адреса + При включении проверяет IP-адреса, возвращаемые для региональных доменов (например, geosite:cn - geoip:cn), и оставляет только ожидаемые IP-адреса Включить пользовательский DNS @@ -1653,4 +1647,22 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Certificate fingerprint (SHA-256) + + Serve Stale + + + Parallel Query + + + By default, invoked only during routing for resolution + + + By default, invoked only during routing for resolution; ensure the remote server can reach this DNS + + + If unset or "AsIs", DNS resolution uses the system DNS; otherwise, the internal DNS module is used. + + + If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. + \ 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 b37e0ccb..b1675d48 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1416,17 +1416,11 @@ 直连 DNS - - 通过代理,请确保远程可用 + + 直连目标解析策略 - - xray freedom 解析策略 - - - sing-box 直连解析策略 - - - sing-box 远程解析策略 + + 代理目标解析策略 添加常用 DNS Hosts @@ -1450,7 +1444,7 @@ 校验相应地区域名 IP - 配置后,会对相应地区域名(如 geosite:cn)的返回 IP 进行校验,仅返回期望 IP + 配置后,会对相应地区域名(如 geosite:cn - geoip:cn)的返回 IP 进行校验,仅返回期望 IP 启用自定义 DNS @@ -1650,4 +1644,22 @@ 证书指纹(SHA-256) + + 乐观缓存 + + + 并行查询 + + + 默认仅在路由阶段被调用解析 + + + 默认仅在路由阶段被调用解析;请确保远程服务器可访问该 DNS + + + 当未选择或 "AsIs" 时,使用系统 DNS 进行解析;否则,使用内部 DNS 模块解析。 + + + 当未选择或 "AsIs" 时,由远程服务器端 DNS 解析;否则,使用内部 DNS 模块解析。 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index b5acd049..73a7dfe5 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1416,17 +1416,11 @@ 直連 DNS - - 通过代理,请确保远程可用 + + 直連目標解析策略 - - xray freedom 解析策略 - - - sing-box 直連解析策略 - - - sing-box 遠程解析策略 + + 代理目標解析策略 新增常用 DNS Hosts @@ -1450,7 +1444,7 @@ 校驗相應地區域名 IP - 配置後,會對相應地區域名(如 geosite:cn)的返回 IP 進行校驗,僅返回期望 IP + 配置後,會對相應地區域名(如 geosite:cn - geoip:cn)的返回 IP 進行校驗,僅返回期望 IP 啟用自訂 DNS @@ -1650,4 +1644,22 @@ Certificate fingerprint (SHA-256) + + Serve Stale + + + Parallel Query + + + By default, invoked only during routing for resolution + + + By default, invoked only during routing for resolution; ensure the remote server can reach this DNS + + + If unset or "AsIs", DNS resolution uses the system DNS; otherwise, the internal DNS module is used. + + + If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index fa089140..6079b2fb 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -175,18 +175,18 @@ public partial class CoreConfigSingboxService singboxConfig.dns.rules ??= new List(); singboxConfig.dns.rules.AddRange(new[] - { + { new Rule4Sbox { ip_accept_any = true, server = Global.SingboxHostsDNSTag }, new Rule4Sbox { server = Global.SingboxRemoteDNSTag, - strategy = simpleDNSItem.SingboxStrategy4Proxy.NullIfEmpty(), + strategy = Utils.DomainStrategy4Sbox(simpleDNSItem.Strategy4Proxy), clash_mode = ERuleMode.Global.ToString() }, new Rule4Sbox { server = Global.SingboxDirectDNSTag, - strategy = simpleDNSItem.SingboxStrategy4Direct.NullIfEmpty(), + strategy = Utils.DomainStrategy4Sbox(simpleDNSItem.Strategy4Freedom), clash_mode = ERuleMode.Direct.ToString() } }); @@ -309,7 +309,7 @@ public partial class CoreConfigSingboxService if (item.OutboundTag == Global.DirectTag) { rule.server = Global.SingboxDirectDNSTag; - rule.strategy = string.IsNullOrEmpty(simpleDNSItem.SingboxStrategy4Direct) ? null : simpleDNSItem.SingboxStrategy4Direct; + rule.strategy = Utils.DomainStrategy4Sbox(simpleDNSItem.Strategy4Freedom); if (expectedIPsRegions.Count > 0 && rule.geosite?.Count > 0) { @@ -343,7 +343,7 @@ public partial class CoreConfigSingboxService singboxConfig.dns.rules.Add(rule4Fake); } rule.server = Global.SingboxRemoteDNSTag; - rule.strategy = string.IsNullOrEmpty(simpleDNSItem.SingboxStrategy4Proxy) ? null : simpleDNSItem.SingboxStrategy4Proxy; + rule.strategy = Utils.DomainStrategy4Sbox(simpleDNSItem.Strategy4Proxy); } singboxConfig.dns.rules.Add(rule); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index d53f29f8..cb4a2af1 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -7,21 +7,21 @@ public partial class CoreConfigSingboxService try { singboxConfig.route.final = Global.ProxyTag; - var item = _config.SimpleDNSItem; + var simpleDnsItem = _config.SimpleDNSItem; var defaultDomainResolverTag = Global.SingboxDirectDNSTag; - var directDNSStrategy = item.SingboxStrategy4Direct.IsNullOrEmpty() ? Global.SingboxDomainStrategy4Out.FirstOrDefault() : item.SingboxStrategy4Direct; + var directDnsStrategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom); var rawDNSItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); - if (rawDNSItem != null && rawDNSItem.Enabled == true) + if (rawDNSItem is { Enabled: true }) { defaultDomainResolverTag = Global.SingboxLocalDNSTag; - directDNSStrategy = rawDNSItem.DomainStrategy4Freedom.IsNullOrEmpty() ? Global.SingboxDomainStrategy4Out.FirstOrDefault() : rawDNSItem.DomainStrategy4Freedom; + directDnsStrategy = rawDNSItem.DomainStrategy4Freedom.IsNullOrEmpty() ? null : rawDNSItem.DomainStrategy4Freedom; } singboxConfig.route.default_domain_resolver = new() { server = defaultDomainResolverTag, - strategy = directDNSStrategy + strategy = directDnsStrategy }; if (_config.TunModeItem.EnableTun) @@ -73,18 +73,17 @@ public partial class CoreConfigSingboxService var hostsDomains = new List(); var dnsItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); - if (dnsItem == null || dnsItem.Enabled == false) + if (dnsItem == null || !dnsItem.Enabled) { - var simpleDNSItem = _config.SimpleDNSItem; - if (!simpleDNSItem.Hosts.IsNullOrEmpty()) + if (!simpleDnsItem.Hosts.IsNullOrEmpty()) { - var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts); + var userHostsMap = Utils.ParseHostsToDictionary(simpleDnsItem.Hosts); foreach (var kvp in userHostsMap) { hostsDomains.Add(kvp.Key); } } - if (simpleDNSItem.UseSystemHosts == true) + if (simpleDnsItem.UseSystemHosts == true) { var systemHostsMap = Utils.GetSystemHosts(); foreach (var kvp in systemHostsMap) diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs index 6ba45895..2329c395 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs @@ -7,48 +7,74 @@ public partial class CoreConfigV2rayService try { var item = await AppManager.Instance.GetDNSItem(ECoreType.Xray); - if (item != null && item.Enabled == true) + if (item is { Enabled: true }) { var result = await GenDnsCompatible(node, v2rayConfig); - if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch) + if (v2rayConfig.routing.domainStrategy != Global.IPIfNonMatch) { - // DNS routing - v2rayConfig.dns.tag = Global.DnsTag; - v2rayConfig.routing.rules.Add(new RulesItem4Ray - { - type = "field", - inboundTag = new List { Global.DnsTag }, - outboundTag = Global.ProxyTag, - }); + return result; } + // DNS routing + var dnsObj = JsonUtils.SerializeToNode(v2rayConfig.dns); + if (dnsObj == null) + { + return result; + } + + dnsObj["tag"] = Global.DnsTag; + v2rayConfig.dns = JsonUtils.Deserialize(JsonUtils.Serialize(dnsObj)); + v2rayConfig.routing.rules.Add(new RulesItem4Ray + { + type = "field", + inboundTag = new List { Global.DnsTag }, + outboundTag = Global.ProxyTag, + }); + return result; } - var simpleDNSItem = _config.SimpleDNSItem; - var domainStrategy4Freedom = simpleDNSItem?.RayStrategy4Freedom; + var simpleDnsItem = _config.SimpleDNSItem; + var dnsItem = v2rayConfig.dns is Dns4Ray dns4Ray ? dns4Ray : new Dns4Ray(); + var strategy4Freedom = simpleDnsItem?.Strategy4Freedom ?? Global.AsIs; //Outbound Freedom domainStrategy - if (domainStrategy4Freedom.IsNotEmpty()) + if (strategy4Freedom.IsNotEmpty() && strategy4Freedom != Global.AsIs) { var outbound = v2rayConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag }); if (outbound != null) { outbound.settings = new() { - domainStrategy = domainStrategy4Freedom, + domainStrategy = strategy4Freedom, userLevel = 0 }; } } - await GenDnsServers(node, v2rayConfig, simpleDNSItem); - await GenDnsHosts(v2rayConfig, simpleDNSItem); + var strategy4Proxy = simpleDnsItem?.Strategy4Proxy ?? Global.AsIs; + //Outbound Proxy domainStrategy + if (strategy4Proxy.IsNotEmpty() && strategy4Proxy != Global.AsIs) + { + var xraySupportConfigTypeNames = Global.XraySupportConfigType + .Select(x => x == EConfigType.Hysteria2 ? "hysteria" : Global.ProtocolTypes[x]) + .ToHashSet(); + v2rayConfig.outbounds + .Where(t => xraySupportConfigTypeNames.Contains(t.protocol)) + .ToList() + .ForEach(outbound => outbound.targetStrategy = strategy4Proxy); + } + + await GenDnsServers(node, dnsItem, simpleDnsItem); + await GenDnsHosts(dnsItem, simpleDnsItem); + + dnsItem.serveStale = simpleDnsItem?.ServeStale is true ? true : null; + dnsItem.enableParallelQuery = simpleDnsItem?.ParallelQuery is true ? true : null; if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch) { // DNS routing - v2rayConfig.dns.tag = Global.DnsTag; + dnsItem.tag = Global.DnsTag; v2rayConfig.routing.rules.Add(new RulesItem4Ray { type = "field", @@ -56,6 +82,8 @@ public partial class CoreConfigV2rayService outboundTag = Global.ProxyTag, }); } + + v2rayConfig.dns = dnsItem; } catch (Exception ex) { @@ -64,7 +92,7 @@ public partial class CoreConfigV2rayService return 0; } - private async Task GenDnsServers(ProfileItem? node, V2rayConfig v2rayConfig, SimpleDNSItem simpleDNSItem) + private async Task GenDnsServers(ProfileItem? node, Dns4Ray dnsItem, SimpleDNSItem simpleDNSItem) { static List ParseDnsAddresses(string? dnsInput, string defaultAddress) { @@ -77,7 +105,7 @@ public partial class CoreConfigV2rayService return addresses.Count > 0 ? addresses : new List { defaultAddress }; } - static object CreateDnsServer(string dnsAddress, List domains, List? expectedIPs = null) + static object? CreateDnsServer(string dnsAddress, List domains, List? expectedIPs = null) { var (domain, scheme, port, path) = Utils.ParseUrl(dnsAddress); var domainFinal = dnsAddress; @@ -106,8 +134,8 @@ public partial class CoreConfigV2rayService }); } - var directDNSAddress = ParseDnsAddresses(simpleDNSItem?.DirectDNS, Global.DomainDirectDNSAddress.FirstOrDefault()); - var remoteDNSAddress = ParseDnsAddresses(simpleDNSItem?.RemoteDNS, Global.DomainRemoteDNSAddress.FirstOrDefault()); + var directDNSAddress = ParseDnsAddresses(simpleDNSItem?.DirectDNS, Global.DomainDirectDNSAddress.First()); + var remoteDNSAddress = ParseDnsAddresses(simpleDNSItem?.RemoteDNS, Global.DomainRemoteDNSAddress.First()); var directDomainList = new List(); var directGeositeList = new List(); @@ -117,7 +145,7 @@ public partial class CoreConfigV2rayService var expectedIPs = new List(); var regionNames = new HashSet(); - var bootstrapDNSAddress = ParseDnsAddresses(simpleDNSItem?.BootstrapDNS, Global.DomainPureIPDNSAddress.FirstOrDefault()); + var bootstrapDNSAddress = ParseDnsAddresses(simpleDNSItem?.BootstrapDNS, Global.DomainPureIPDNSAddress.First()); var dnsServerDomains = new List(); foreach (var dns in directDNSAddress) @@ -171,51 +199,48 @@ public partial class CoreConfigV2rayService var routing = await ConfigHandler.GetDefaultRouting(_config); List? rules = null; - if (routing != null) + rules = JsonUtils.Deserialize>(routing.RuleSet) ?? []; + foreach (var item in rules) { - rules = JsonUtils.Deserialize>(routing.RuleSet) ?? []; - foreach (var item in rules) + if (!item.Enabled || item.Domain is null || item.Domain.Count == 0) { - if (!item.Enabled || item.Domain is null || item.Domain.Count == 0) + continue; + } + + if (item.RuleType == ERuleType.Routing) + { + continue; + } + + foreach (var domain in item.Domain) + { + if (domain.StartsWith('#')) { continue; } - if (item.RuleType == ERuleType.Routing) + var normalizedDomain = domain.Replace(Global.RoutingRuleComma, ","); + + if (item.OutboundTag == Global.DirectTag) { - continue; + if (normalizedDomain.StartsWith("geosite:") || normalizedDomain.StartsWith("ext:")) + { + (regionNames.Contains(normalizedDomain) ? expectedDomainList : directGeositeList).Add(normalizedDomain); + } + else + { + directDomainList.Add(normalizedDomain); + } } - - foreach (var domain in item.Domain) + else if (item.OutboundTag != Global.BlockTag) { - if (domain.StartsWith('#')) + if (normalizedDomain.StartsWith("geosite:") || normalizedDomain.StartsWith("ext:")) { - continue; + proxyGeositeList.Add(normalizedDomain); } - - var normalizedDomain = domain.Replace(Global.RoutingRuleComma, ","); - - if (item.OutboundTag == Global.DirectTag) + else { - if (normalizedDomain.StartsWith("geosite:") || normalizedDomain.StartsWith("ext:")) - { - (regionNames.Contains(normalizedDomain) ? expectedDomainList : directGeositeList).Add(normalizedDomain); - } - else - { - directDomainList.Add(normalizedDomain); - } - } - else if (item.OutboundTag != Global.BlockTag) - { - if (normalizedDomain.StartsWith("geosite:") || normalizedDomain.StartsWith("ext:")) - { - proxyGeositeList.Add(normalizedDomain); - } - else - { - proxyDomainList.Add(normalizedDomain); - } + proxyDomainList.Add(normalizedDomain); } } } @@ -244,8 +269,7 @@ public partial class CoreConfigV2rayService } } - v2rayConfig.dns ??= new Dns4Ray(); - v2rayConfig.dns.servers ??= new List(); + dnsItem.servers ??= []; void AddDnsServers(List dnsAddresses, List domains, List? expectedIPs = null) { @@ -253,7 +277,7 @@ public partial class CoreConfigV2rayService { foreach (var dnsAddress in dnsAddresses) { - v2rayConfig.dns.servers.Add(CreateDnsServer(dnsAddress, domains, expectedIPs)); + dnsItem.servers.Add(CreateDnsServer(dnsAddress, domains, expectedIPs)); } } } @@ -275,22 +299,21 @@ public partial class CoreConfigV2rayService || lastRule.Ip?.Contains("0.0.0.0/0") == true); var defaultDnsServers = useDirectDns ? directDNSAddress : remoteDNSAddress; - v2rayConfig.dns.servers.AddRange(defaultDnsServers); + dnsItem.servers.AddRange(defaultDnsServers); return 0; } - private async Task GenDnsHosts(V2rayConfig v2rayConfig, SimpleDNSItem simpleDNSItem) + private async Task GenDnsHosts(Dns4Ray dnsItem, SimpleDNSItem simpleDNSItem) { if (simpleDNSItem.AddCommonHosts == false && simpleDNSItem.UseSystemHosts == false && simpleDNSItem.Hosts.IsNullOrEmpty()) { return await Task.FromResult(0); } - v2rayConfig.dns ??= new Dns4Ray(); - v2rayConfig.dns.hosts ??= new Dictionary(); + dnsItem.hosts ??= new Dictionary(); if (simpleDNSItem.AddCommonHosts == true) { - v2rayConfig.dns.hosts = Global.PredefinedHosts.ToDictionary( + dnsItem.hosts = Global.PredefinedHosts.ToDictionary( kvp => kvp.Key, kvp => (object)kvp.Value ); @@ -299,7 +322,7 @@ public partial class CoreConfigV2rayService if (simpleDNSItem.UseSystemHosts == true) { var systemHosts = Utils.GetSystemHosts(); - var normalHost = v2rayConfig?.dns?.hosts; + var normalHost = dnsItem.hosts; if (normalHost != null && systemHosts?.Count > 0) { @@ -316,7 +339,7 @@ public partial class CoreConfigV2rayService foreach (var kvp in userHostsMap) { - v2rayConfig.dns.hosts[kvp.Key] = kvp.Value; + dnsItem.hosts[kvp.Key] = kvp.Value; } } return await Task.FromResult(0); diff --git a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs index d53b960d..00178d17 100644 --- a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs @@ -9,11 +9,12 @@ public class DNSSettingViewModel : MyReactiveObject [Reactive] public string? DirectDNS { get; set; } [Reactive] public string? RemoteDNS { 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; } + [Reactive] public string? Strategy4Freedom { get; set; } + [Reactive] public string? Strategy4Proxy { get; set; } [Reactive] public string? Hosts { get; set; } [Reactive] public string? DirectExpectedIPs { get; set; } + [Reactive] public bool? ParallelQuery { get; set; } + [Reactive] public bool? ServeStale { get; set; } [Reactive] public bool UseSystemHostsCompatible { get; set; } [Reactive] public string DomainStrategy4FreedomCompatible { get; set; } @@ -70,11 +71,12 @@ public class DNSSettingViewModel : MyReactiveObject DirectDNS = item.DirectDNS; RemoteDNS = item.RemoteDNS; BootstrapDNS = item.BootstrapDNS; - RayStrategy4Freedom = item.RayStrategy4Freedom; - SingboxStrategy4Direct = item.SingboxStrategy4Direct; - SingboxStrategy4Proxy = item.SingboxStrategy4Proxy; + Strategy4Freedom = item.Strategy4Freedom; + Strategy4Proxy = item.Strategy4Proxy; Hosts = item.Hosts; DirectExpectedIPs = item.DirectExpectedIPs; + ParallelQuery = item.ParallelQuery; + ServeStale = item.ServeStale; var item1 = await AppManager.Instance.GetDNSItem(ECoreType.Xray); RayCustomDNSEnableCompatible = item1.Enabled; @@ -100,11 +102,12 @@ public class DNSSettingViewModel : MyReactiveObject _config.SimpleDNSItem.DirectDNS = DirectDNS; _config.SimpleDNSItem.RemoteDNS = RemoteDNS; _config.SimpleDNSItem.BootstrapDNS = BootstrapDNS; - _config.SimpleDNSItem.RayStrategy4Freedom = RayStrategy4Freedom; - _config.SimpleDNSItem.SingboxStrategy4Direct = SingboxStrategy4Direct; - _config.SimpleDNSItem.SingboxStrategy4Proxy = SingboxStrategy4Proxy; + _config.SimpleDNSItem.Strategy4Freedom = Strategy4Freedom; + _config.SimpleDNSItem.Strategy4Proxy = Strategy4Proxy; _config.SimpleDNSItem.Hosts = Hosts; _config.SimpleDNSItem.DirectExpectedIPs = DirectExpectedIPs; + _config.SimpleDNSItem.ParallelQuery = ParallelQuery; + _config.SimpleDNSItem.ServeStale = ServeStale; if (NormalDNSCompatible.IsNotEmpty()) { diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml index e5a785b7..705e79e2 100644 --- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml @@ -61,7 +61,15 @@ Grid.Column="1" Width="300" Margin="{StaticResource Margin4}" + VerticalAlignment="Center" IsEditable="True" /> + - + + Text="{x:Static resx:ResUI.TbDirectResolveStrategy}" /> - - + Text="{x:Static resx:ResUI.TbDirectResolveStrategyTips}" + TextWrapping="Wrap" /> + Text="{x:Static resx:ResUI.TbRemoteResolveStrategy}" /> + + Text="{x:Static resx:ResUI.TbParallelQuery}" /> + HorizontalAlignment="Left" + VerticalAlignment="Center" /> + + + @@ -169,7 +196,7 @@ x:Name="gridAdvancedDNSSettings" Margin="{StaticResource Margin8}" ColumnDefinitions="Auto,Auto,*" - RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,*"> + RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,*"> + HorizontalAlignment="Left" + VerticalAlignment="Center" /> + Text="{x:Static resx:ResUI.TbAddCommonDNSHosts}" /> + HorizontalAlignment="Left" + VerticalAlignment="Center" /> + + + + HorizontalAlignment="Left" + VerticalAlignment="Center" /> btnCancel.Click += (s, e) => Close(); ViewModel = new DNSSettingViewModel(UpdateViewHandler); - cmbRayFreedomDNSStrategy.ItemsSource = Global.DomainStrategy4Freedoms; - cmbSBDirectDNSStrategy.ItemsSource = Global.SingboxDomainStrategy4Out; - cmbSBRemoteDNSStrategy.ItemsSource = Global.SingboxDomainStrategy4Out; + cmbDirectDNSStrategy.ItemsSource = Global.DomainStrategy; + cmbRemoteDNSStrategy.ItemsSource = Global.DomainStrategy; cmbDirectDNS.ItemsSource = Global.DomainDirectDNSAddress; cmbRemoteDNS.ItemsSource = Global.DomainRemoteDNSAddress; cmbBootstrapDNS.ItemsSource = Global.DomainPureIPDNSAddress; cmbDirectExpectedIPs.ItemsSource = Global.ExpectedIPs; - cmbdomainStrategy4FreedomCompatible.ItemsSource = Global.DomainStrategy4Freedoms; - cmbdomainStrategy4OutCompatible.ItemsSource = Global.SingboxDomainStrategy4Out; + cmbdomainStrategy4FreedomCompatible.ItemsSource = Global.DomainStrategy; + cmbdomainStrategy4OutCompatible.ItemsSource = Global.DomainStrategies4Sbox; cmbdomainDNSAddressCompatible.ItemsSource = Global.DomainPureIPDNSAddress; cmbdomainDNSAddress2Compatible.ItemsSource = Global.DomainPureIPDNSAddress; @@ -37,11 +36,12 @@ public partial class DNSSettingWindow : WindowBase 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.Strategy4Freedom, v => v.cmbDirectDNSStrategy.SelectedItem).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Strategy4Proxy, v => v.cmbRemoteDNSStrategy.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.ParallelQuery, v => v.togParallelQuery.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.ServeStale, v => v.togServeStale.IsChecked).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); diff --git a/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs index 076307ec..c2d90a33 100644 --- a/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml.cs @@ -26,7 +26,7 @@ public partial class RoutingRuleSettingWindow : WindowBase { diff --git a/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml.cs index 719acf94..e25f7237 100644 --- a/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/RoutingSettingWindow.axaml.cs @@ -22,7 +22,7 @@ public partial class RoutingSettingWindow : WindowBase ViewModel = new RoutingSettingViewModel(UpdateViewHandler); cmbdomainStrategy.ItemsSource = Global.DomainStrategies; - cmbdomainStrategy4Singbox.ItemsSource = Global.DomainStrategies4Singbox; + cmbdomainStrategy4Singbox.ItemsSource = Global.DomainStrategies4Sbox; this.WhenActivated(disposables => { diff --git a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml index 7deb7c25..8788e97d 100644 --- a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml +++ b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml @@ -84,6 +84,14 @@ Margin="{StaticResource Margin8}" IsEditable="True" Style="{StaticResource DefComboBox}" /> + + Text="{x:Static resx:ResUI.TbDirectResolveStrategy}" /> - - + Text="{x:Static resx:ResUI.TbDirectResolveStrategyTips}" + TextWrapping="Wrap" /> + Text="{x:Static resx:ResUI.TbRemoteResolveStrategy}" /> + + Text="{x:Static resx:ResUI.TbParallelQuery}" /> + + + @@ -207,6 +229,7 @@ + @@ -245,15 +268,29 @@ Margin="{StaticResource Margin8}" VerticalAlignment="Center" Style="{StaticResource ToolbarTextBlock}" - Text="{x:Static resx:ResUI.TbFakeIP}" /> + Text="{x:Static resx:ResUI.TbAddCommonDNSHosts}" /> + + + 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.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SingboxStrategy4Direct, v => v.cmbSBDirectDNSStrategy.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SingboxStrategy4Proxy, v => v.cmbSBRemoteDNSStrategy.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Strategy4Freedom, v => v.cmbDirectDNSStrategy.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Strategy4Proxy, v => v.cmbRemoteDNSStrategy.Text).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.ParallelQuery, v => v.togParallelQuery.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.ServeStale, v => v.togServeStale.IsChecked).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); diff --git a/v2rayN/v2rayN/Views/RoutingRuleSettingWindow.xaml.cs b/v2rayN/v2rayN/Views/RoutingRuleSettingWindow.xaml.cs index fd0cee6e..e736fb6b 100644 --- a/v2rayN/v2rayN/Views/RoutingRuleSettingWindow.xaml.cs +++ b/v2rayN/v2rayN/Views/RoutingRuleSettingWindow.xaml.cs @@ -18,7 +18,7 @@ public partial class RoutingRuleSettingWindow ViewModel = new RoutingRuleSettingViewModel(routingItem, UpdateViewHandler); cmbdomainStrategy.ItemsSource = Global.DomainStrategies.AppendEmpty(); - cmbdomainStrategy4Singbox.ItemsSource = Global.DomainStrategies4Singbox; + cmbdomainStrategy4Singbox.ItemsSource = Global.DomainStrategies4Sbox; this.WhenActivated(disposables => { diff --git a/v2rayN/v2rayN/Views/RoutingSettingWindow.xaml.cs b/v2rayN/v2rayN/Views/RoutingSettingWindow.xaml.cs index 98ccdc19..a9a781a4 100644 --- a/v2rayN/v2rayN/Views/RoutingSettingWindow.xaml.cs +++ b/v2rayN/v2rayN/Views/RoutingSettingWindow.xaml.cs @@ -17,7 +17,7 @@ public partial class RoutingSettingWindow ViewModel = new RoutingSettingViewModel(UpdateViewHandler); cmbdomainStrategy.ItemsSource = Global.DomainStrategies; - cmbdomainStrategy4Singbox.ItemsSource = Global.DomainStrategies4Singbox; + cmbdomainStrategy4Singbox.ItemsSource = Global.DomainStrategies4Sbox; this.WhenActivated(disposables => { From 018d541910c89df779dfbd95780c81ccc6e21c88 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Wed, 4 Feb 2026 06:35:40 +0000 Subject: [PATCH 14/24] Update CA List (#8755) --- v2rayN/ServiceLib/Manager/CertPemManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/v2rayN/ServiceLib/Manager/CertPemManager.cs b/v2rayN/ServiceLib/Manager/CertPemManager.cs index dfefc746..bb8df227 100644 --- a/v2rayN/ServiceLib/Manager/CertPemManager.cs +++ b/v2rayN/ServiceLib/Manager/CertPemManager.cs @@ -197,6 +197,7 @@ public class CertPemManager "D02A0F994A868C66395F2E7A880DF509BD0C29C96DE16015A0FD501EDA4F96A9", // OISTE Client Root RSA G1 "EEC997C0C30F216F7E3B8B307D2BAE42412D753FC8219DAFD1520B2572850F49", // OISTE Server Root ECC G1 "9AE36232A5189FFDDB353DFD26520C015395D22777DAC59DB57B98C089A651E6", // OISTE Server Root RSA G1 + "B49141502D00663D740F2E7EC340C52800962666121A36D09CF7DD2B90384FB4", // e-Szigno TLS Root CA 2023 }; /// From 8d01d8fda5e1f10f8ed9264eb6e9d0cb0c30d735 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:13:10 +0800 Subject: [PATCH 15/24] Remove windows-64-SelfContained build/artifact steps --- .github/workflows/build-windows.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 9624ba43..d1c2f66a 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -15,7 +15,6 @@ env: OutputArchArm: "windows-arm64" OutputPath64: "${{ github.workspace }}/v2rayN/Release/windows-64" OutputPathArm64: "${{ github.workspace }}/v2rayN/Release/windows-arm64" - OutputPath64Sc: "${{ github.workspace }}/v2rayN/Release/windows-64-SelfContained" jobs: build: @@ -39,11 +38,8 @@ jobs: cd v2rayN dotnet publish ./v2rayN/v2rayN.csproj -c Release -r win-x64 -p:SelfContained=true -p:EnableWindowsTargeting=true -o $OutputPath64 dotnet publish ./v2rayN/v2rayN.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -o $OutputPathArm64 - dotnet publish ./v2rayN/v2rayN.csproj -c Release -r win-x64 -p:SelfContained=true -p:EnableWindowsTargeting=true -o $OutputPath64Sc dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-x64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPath64 dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-arm64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPathArm64 - dotnet publish ./AmazTool/AmazTool.csproj -c Release -r win-x64 -p:SelfContained=true -p:EnableWindowsTargeting=true -p:PublishTrimmed=true -o $OutputPath64Sc - - name: Upload build artifacts uses: actions/upload-artifact@v6.0.0 @@ -59,7 +55,6 @@ jobs: chmod 755 package-release-zip.sh ./package-release-zip.sh $OutputArch $OutputPath64 ./package-release-zip.sh $OutputArchArm $OutputPathArm64 - ./package-release-zip.sh "windows-64-SelfContained" $OutputPath64Sc - name: Upload zip archive to release uses: svenstaro/upload-release-action@v2 From fdb733fa726f938827284e069e504374ac962c93 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:19:54 +0800 Subject: [PATCH 16/24] up 7.18.0 --- v2rayN/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2rayN/Directory.Build.props b/v2rayN/Directory.Build.props index d44c63bd..e7e49e2c 100644 --- a/v2rayN/Directory.Build.props +++ b/v2rayN/Directory.Build.props @@ -1,7 +1,7 @@ - 7.17.3 + 7.18.0 From bceebc1661055bbb24e78be8fb3234b8fe5f9c32 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:52:48 +0800 Subject: [PATCH 17/24] Add process to routing domains https://github.com/2dust/v2rayN/issues/8757 --- v2rayN/ServiceLib/Common/Utils.cs | 14 +++++++++++++- .../CoreConfig/Singbox/SingboxRoutingService.cs | 8 ++------ .../CoreConfig/V2ray/V2rayRoutingService.cs | 2 +- .../ViewModels/RoutingRuleSettingViewModel.cs | 2 +- .../Views/RoutingRuleSettingWindow.axaml | 2 +- v2rayN/v2rayN/Views/RoutingRuleSettingWindow.xaml | 2 +- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index 7824ff4a..7a3fa9a0 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -1086,7 +1086,19 @@ public class Utils public static string GetExeName(string name) { - return IsWindows() ? $"{name}.exe" : name; + if (name.IsNullOrEmpty() || IsNonWindows()) + { + return name; + } + + if (name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) + { + return name; + } + else + { + return $"{name}.exe"; + } } public static bool IsAdministrator() diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index cb4a2af1..0370ce29 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -277,7 +277,7 @@ public partial class CoreConfigSingboxService } } - if (_config.TunModeItem.EnableTun && item.Process?.Count > 0) + if (item.Process?.Count > 0) { var ruleProcName = JsonUtils.DeepCopy(rule3); ruleProcName.process_name ??= []; @@ -304,11 +304,7 @@ public partial class CoreConfigSingboxService } // sing-box strictly matches the exe suffix on Windows - var procName = process; - if (Utils.IsWindows() && !procName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) - { - procName += ".exe"; - } + var procName = Utils.GetExeName(process); ruleProcName.process_name.Add(procName); } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs index 96984094..8691abb1 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs @@ -109,7 +109,7 @@ public partial class CoreConfigV2rayService v2rayConfig.routing.rules.Add(it); hasDomainIp = true; } - if (_config.TunModeItem.EnableTun && rule.process?.Count > 0) + if (rule.process?.Count > 0) { var it = JsonUtils.DeepCopy(rule); it.domain = null; diff --git a/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs index 71d42218..3baf79c6 100644 --- a/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs @@ -106,7 +106,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject Network = item.Network, Protocols = Utils.List2String(item.Protocol), InboundTags = Utils.List2String(item.InboundTag), - Domains = Utils.List2String((item.Domain ?? []).Concat(item.Ip ?? []).ToList()), + Domains = Utils.List2String((item.Domain ?? []).Concat(item.Ip ?? []).ToList().Concat(item.Process ?? []).ToList()), Enabled = item.Enabled, Remarks = item.Remarks, }; diff --git a/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml index 7d00b3cc..a770b3ed 100644 --- a/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/RoutingRuleSettingWindow.axaml @@ -248,7 +248,7 @@ + Header="domain / ip / process" /> diff --git a/v2rayN/v2rayN/Views/RoutingRuleSettingWindow.xaml b/v2rayN/v2rayN/Views/RoutingRuleSettingWindow.xaml index c8e679a8..83238cb7 100644 --- a/v2rayN/v2rayN/Views/RoutingRuleSettingWindow.xaml +++ b/v2rayN/v2rayN/Views/RoutingRuleSettingWindow.xaml @@ -333,7 +333,7 @@ + Header="domain / ip / process" /> From d9843dc77502454b1ec48cec6244e115f1abd082 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Thu, 5 Feb 2026 09:05:27 +0000 Subject: [PATCH 18/24] Add DNS Hosts features (#8756) --- v2rayN/ServiceLib/Common/Utils.cs | 18 ++--- .../CoreConfig/Singbox/SingboxDnsService.cs | 77 ++++++++++++++----- .../Singbox/SingboxRoutingService.cs | 10 +-- .../CoreConfig/V2ray/V2rayDnsService.cs | 9 +-- 4 files changed, 70 insertions(+), 44 deletions(-) diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index 7a3fa9a0..c538f9c8 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -332,22 +332,20 @@ public class Utils .ToList(); } - public static Dictionary> ParseHostsToDictionary(string hostsContent) + public static Dictionary> ParseHostsToDictionary(string? hostsContent) { + if (hostsContent.IsNullOrEmpty()) + { + return new(); + } var userHostsMap = hostsContent - .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) .Select(line => line.Trim()) // skip full-line comments - .Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#")) - // strip inline comments (truncate at '#') - .Select(line => - { - var index = line.IndexOf('#'); - return index >= 0 ? line.Substring(0, index).Trim() : line; - }) + .Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith('#')) // ensure line still contains valid parts .Where(line => !string.IsNullOrWhiteSpace(line) && line.Contains(' ')) - .Select(line => line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries)) + .Select(line => line.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries)) .Where(parts => parts.Length >= 2) .GroupBy(parts => parts[0]) .ToDictionary( diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index 6079b2fb..7684cbd1 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -88,14 +88,9 @@ public partial class CoreConfigSingboxService } } - if (!simpleDNSItem.Hosts.IsNullOrEmpty()) + foreach (var kvp in Utils.ParseHostsToDictionary(simpleDNSItem.Hosts)) { - var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts); - - foreach (var kvp in userHostsMap) - { - hostsDns.predefined[kvp.Key] = kvp.Value; - } + hostsDns.predefined[kvp.Key] = kvp.Value.Where(s => Utils.IsIpAddress(s)).ToList(); } foreach (var host in hostsDns.predefined) @@ -115,7 +110,7 @@ public partial class CoreConfigSingboxService } singboxConfig.dns ??= new Dns4Sbox(); - singboxConfig.dns.servers ??= new List(); + singboxConfig.dns.servers ??= []; singboxConfig.dns.servers.Add(remoteDns); singboxConfig.dns.servers.Add(directDns); singboxConfig.dns.servers.Add(hostsDns); @@ -191,13 +186,56 @@ public partial class CoreConfigSingboxService } }); + foreach (var kvp in Utils.ParseHostsToDictionary(simpleDNSItem.Hosts)) + { + var predefined = kvp.Value.First(); + if (predefined.IsNullOrEmpty() || Utils.IsIpAddress(predefined)) + { + continue; + } + if (predefined.StartsWith('#') && int.TryParse(predefined.AsSpan(1), out var rcode)) + { + // xray syntactic sugar for predefined + // etc. #0 -> NOERROR + singboxConfig.dns.rules.Add(new() + { + query_type = [1, 28], + domain = [kvp.Key], + action = "predefined", + rcode = rcode switch + { + 0 => "NOERROR", + 1 => "FORMERR", + 2 => "SERVFAIL", + 3 => "NXDOMAIN", + 4 => "NOTIMP", + 5 => "REFUSED", + _ => "NOERROR", + }, + }); + continue; + } + // CNAME record + Rule4Sbox rule = new() + { + query_type = [1, 28], + action = "predefined", + rcode = "NOERROR", + answer = [$"*. IN CNAME {predefined}."], + }; + if (ParseV2Domain(kvp.Key, rule)) + { + singboxConfig.dns.rules.Add(rule); + } + } + var (ech, _) = ParseEchParam(node?.EchConfigList); if (ech is not null) { var echDomain = ech.query_server_name ?? node?.Sni; singboxConfig.dns.rules.Add(new() { - query_type = new List { 64, 65 }, + query_type = [64, 65], server = Global.SingboxEchDNSTag, domain = echDomain is not null ? new List { echDomain } : null, }); @@ -209,7 +247,7 @@ public partial class CoreConfigSingboxService { singboxConfig.dns.rules.Add(new() { - query_type = new List { 64, 65 }, + query_type = [64, 65], server = Global.SingboxEchDNSTag, domain = queryServerNames, }); @@ -220,9 +258,9 @@ public partial class CoreConfigSingboxService { singboxConfig.dns.rules.Add(new() { - query_type = new List { 64, 65 }, + query_type = [64, 65], action = "predefined", - rcode = "NOTIMP" + rcode = "NOERROR" }); } @@ -236,13 +274,14 @@ public partial class CoreConfigSingboxService type = "logical", mode = "and", rewrite_ttl = 1, - rules = new List - { - new() { - query_type = new List { 1, 28 }, // A and AAAA + rules = + [ + new() + { + query_type = [1, 28], // A and AAAA }, - fakeipFilterRule, - } + fakeipFilterRule + ] }; singboxConfig.dns.rules.Add(rule4Fake); @@ -262,7 +301,7 @@ public partial class CoreConfigSingboxService if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs)) { var ipItems = simpleDNSItem.DirectExpectedIPs - .Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) + .Split([',', ';'], StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Trim()) .Where(s => !string.IsNullOrEmpty(s)) .ToList(); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index 0370ce29..6198d027 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -75,14 +75,8 @@ public partial class CoreConfigSingboxService var dnsItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box); if (dnsItem == null || !dnsItem.Enabled) { - if (!simpleDnsItem.Hosts.IsNullOrEmpty()) - { - var userHostsMap = Utils.ParseHostsToDictionary(simpleDnsItem.Hosts); - foreach (var kvp in userHostsMap) - { - hostsDomains.Add(kvp.Key); - } - } + var userHostsMap = Utils.ParseHostsToDictionary(simpleDnsItem.Hosts); + hostsDomains.AddRange(userHostsMap.Select(kvp => kvp.Key)); if (simpleDnsItem.UseSystemHosts == true) { var systemHostsMap = Utils.GetSystemHosts(); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs index 2329c395..7de97240 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs @@ -333,14 +333,9 @@ public partial class CoreConfigV2rayService } } - if (!simpleDNSItem.Hosts.IsNullOrEmpty()) + foreach (var kvp in Utils.ParseHostsToDictionary(simpleDNSItem.Hosts)) { - var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts); - - foreach (var kvp in userHostsMap) - { - dnsItem.hosts[kvp.Key] = kvp.Value; - } + dnsItem.hosts[kvp.Key] = kvp.Value; } return await Task.FromResult(0); } From 677e81f9a7d0d14ea18be9039e56554548f1a72a Mon Sep 17 00:00:00 2001 From: DHR60 Date: Thu, 5 Feb 2026 11:48:33 +0000 Subject: [PATCH 19/24] Refactor profile item config (#8659) * Refactor * Add hysteria2 bandwidth and hop interval support * Upgrade config version and rename * Refactor id and security * Refactor flow * Fix hy2 bbr * Fix warning CS0618 * Remove unused code * Fix hy2 migrate * Fix * Refactor * Refactor ProfileItem protocol extra handling * Refactor, use record instead of class * Hy2 SalamanderPass * Fix Tuic * Fix group * Fix Tuic * Fix hy2 Brutal Bandwidth * Clean Code * Fix * Support interval range * Add Username --------- Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com> --- v2rayN/ServiceLib/Global.cs | 2 + v2rayN/ServiceLib/Handler/ConfigHandler.cs | 159 +++---- v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs | 4 +- v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs | 6 - v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs | 20 +- .../ServiceLib/Handler/Fmt/ShadowsocksFmt.cs | 22 +- v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs | 12 +- v2rayN/ServiceLib/Handler/Fmt/TrojanFmt.cs | 9 +- v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs | 6 +- v2rayN/ServiceLib/Handler/Fmt/VLESSFmt.cs | 21 +- v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs | 30 +- v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs | 32 +- .../Manager/ActionPrecheckManager.cs | 43 +- v2rayN/ServiceLib/Manager/AppManager.cs | 129 +++++- .../ServiceLib/Manager/GroupProfileManager.cs | 180 ++++++++ .../Manager/ProfileGroupItemManager.cs | 388 ------------------ v2rayN/ServiceLib/Models/ProfileGroupItem.cs | 1 + v2rayN/ServiceLib/Models/ProfileItem.cs | 61 ++- v2rayN/ServiceLib/Models/ProtocolExtraItem.cs | 38 ++ v2rayN/ServiceLib/Models/V2rayConfig.cs | 2 +- v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 9 + v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.fr.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.hu.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.ru.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 3 + .../CoreConfig/Singbox/SingboxDnsService.cs | 2 +- .../Singbox/SingboxOutboundService.cs | 117 ++++-- .../CoreConfig/V2ray/V2rayOutboundService.cs | 96 +++-- .../ViewModels/AddGroupServerViewModel.cs | 64 ++- .../ViewModels/AddServerViewModel.cs | 80 +++- .../ViewModels/MainWindowViewModel.cs | 1 - .../ViewModels/ProfilesSelectViewModel.cs | 2 +- .../ViewModels/ProfilesViewModel.cs | 2 +- .../Views/AddServerWindow.axaml | 37 +- .../Views/AddServerWindow.axaml.cs | 57 ++- v2rayN/v2rayN/Views/AddServerWindow.xaml | 43 ++ v2rayN/v2rayN/Views/AddServerWindow.xaml.cs | 57 ++- 40 files changed, 951 insertions(+), 802 deletions(-) create mode 100644 v2rayN/ServiceLib/Manager/GroupProfileManager.cs delete mode 100644 v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs create mode 100644 v2rayN/ServiceLib/Models/ProtocolExtraItem.cs diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index 486e53ed..e502435f 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -90,6 +90,8 @@ public class Global public const string SingboxFakeDNSTag = "fake_dns"; public const string SingboxEchDNSTag = "ech_dns"; + public const int Hysteria2DefaultHopInt = 10; + public static readonly List IEProxyProtocols = [ "{ip}:{http_port}", diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index f9ff4494..0ea515ec 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -230,12 +230,8 @@ public static class ConfigHandler item.Remarks = profileItem.Remarks; item.Address = profileItem.Address; item.Port = profileItem.Port; - item.Ports = profileItem.Ports; - item.Id = profileItem.Id; - item.AlterId = profileItem.AlterId; - item.Security = profileItem.Security; - item.Flow = profileItem.Flow; + item.Password = profileItem.Password; item.Network = profileItem.Network; item.HeaderType = profileItem.HeaderType; @@ -258,6 +254,7 @@ public static class ConfigHandler item.CertSha = profileItem.CertSha; item.EchConfigList = profileItem.EchConfigList; item.EchForceQuery = profileItem.EchForceQuery; + item.ProtoExtra = profileItem.ProtoExtra; } var ret = item.ConfigType switch @@ -290,19 +287,22 @@ public static class ConfigHandler profileItem.ConfigType = EConfigType.VMess; profileItem.Address = profileItem.Address.TrimEx(); - profileItem.Id = profileItem.Id.TrimEx(); - profileItem.Security = profileItem.Security.TrimEx(); + profileItem.Password = profileItem.Password.TrimEx(); + profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with + { + VmessSecurity = profileItem.GetProtocolExtra().VmessSecurity?.TrimEx() + }); profileItem.Network = profileItem.Network.TrimEx(); profileItem.HeaderType = profileItem.HeaderType.TrimEx(); profileItem.RequestHost = profileItem.RequestHost.TrimEx(); profileItem.Path = profileItem.Path.TrimEx(); profileItem.StreamSecurity = profileItem.StreamSecurity.TrimEx(); - if (!Global.VmessSecurities.Contains(profileItem.Security)) + if (!Global.VmessSecurities.Contains(profileItem.GetProtocolExtra().VmessSecurity)) { return -1; } - if (profileItem.Id.IsNullOrEmpty()) + if (profileItem.Password.IsNullOrEmpty()) { return -1; } @@ -360,11 +360,6 @@ 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); @@ -610,14 +605,17 @@ public static class ConfigHandler profileItem.ConfigType = EConfigType.Shadowsocks; profileItem.Address = profileItem.Address.TrimEx(); - profileItem.Id = profileItem.Id.TrimEx(); - profileItem.Security = profileItem.Security.TrimEx(); + profileItem.Password = profileItem.Password.TrimEx(); + profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with + { + SsMethod = profileItem.GetProtocolExtra().SsMethod?.TrimEx() + }); - if (!AppManager.Instance.GetShadowsocksSecurities(profileItem).Contains(profileItem.Security)) + if (!AppManager.Instance.GetShadowsocksSecurities(profileItem).Contains(profileItem.GetProtocolExtra().SsMethod)) { return -1; } - if (profileItem.Id.IsNullOrEmpty()) + if (profileItem.Password.IsNullOrEmpty()) { return -1; } @@ -678,12 +676,12 @@ public static class ConfigHandler profileItem.ConfigType = EConfigType.Trojan; profileItem.Address = profileItem.Address.TrimEx(); - profileItem.Id = profileItem.Id.TrimEx(); + profileItem.Password = profileItem.Password.TrimEx(); if (profileItem.StreamSecurity.IsNullOrEmpty()) { profileItem.StreamSecurity = Global.StreamSecurity; } - if (profileItem.Id.IsNullOrEmpty()) + if (profileItem.Password.IsNullOrEmpty()) { return -1; } @@ -708,18 +706,24 @@ public static class ConfigHandler //profileItem.CoreType = ECoreType.sing_box; profileItem.Address = profileItem.Address.TrimEx(); - profileItem.Id = profileItem.Id.TrimEx(); - profileItem.Path = profileItem.Path.TrimEx(); + profileItem.Password = profileItem.Password.TrimEx(); profileItem.Network = string.Empty; if (profileItem.StreamSecurity.IsNullOrEmpty()) { profileItem.StreamSecurity = Global.StreamSecurity; } - if (profileItem.Id.IsNullOrEmpty()) + if (profileItem.Password.IsNullOrEmpty()) { return -1; } + profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with + { + SalamanderPass = profileItem.GetProtocolExtra().SalamanderPass?.TrimEx(), + UpMbps = profileItem.GetProtocolExtra().UpMbps is null or < 0 ? config.HysteriaItem.UpMbps : profileItem.GetProtocolExtra().UpMbps, + DownMbps = profileItem.GetProtocolExtra().DownMbps is null or < 0 ? config.HysteriaItem.DownMbps : profileItem.GetProtocolExtra().DownMbps, + HopInterval = profileItem.GetProtocolExtra().HopInterval?.TrimEx(), + }); await AddServerCommon(config, profileItem, toFile); @@ -741,8 +745,8 @@ public static class ConfigHandler profileItem.CoreType = ECoreType.sing_box; profileItem.Address = profileItem.Address.TrimEx(); - profileItem.Id = profileItem.Id.TrimEx(); - profileItem.Security = profileItem.Security.TrimEx(); + profileItem.Username = profileItem.Username.TrimEx(); + profileItem.Password = profileItem.Password.TrimEx(); profileItem.Network = string.Empty; if (!Global.TuicCongestionControls.Contains(profileItem.HeaderType)) @@ -758,7 +762,7 @@ public static class ConfigHandler { profileItem.Alpn = "h3"; } - if (profileItem.Id.IsNullOrEmpty()) + if (profileItem.Password.IsNullOrEmpty()) { return -1; } @@ -781,17 +785,17 @@ public static class ConfigHandler profileItem.ConfigType = EConfigType.WireGuard; profileItem.Address = profileItem.Address.TrimEx(); - profileItem.Id = profileItem.Id.TrimEx(); - profileItem.PublicKey = profileItem.PublicKey.TrimEx(); - profileItem.Path = profileItem.Path.TrimEx(); - profileItem.RequestHost = profileItem.RequestHost.TrimEx(); - profileItem.Network = string.Empty; - if (profileItem.ShortId.IsNullOrEmpty()) + profileItem.Password = profileItem.Password.TrimEx(); + profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with { - profileItem.ShortId = Global.TunMtus.First().ToString(); - } + WgPublicKey = profileItem.GetProtocolExtra().WgPublicKey?.TrimEx(), + WgPresharedKey = profileItem.GetProtocolExtra().WgPresharedKey?.TrimEx(), + WgInterfaceAddress = profileItem.GetProtocolExtra().WgInterfaceAddress?.TrimEx(), + WgReserved = profileItem.GetProtocolExtra().WgReserved?.TrimEx(), + WgMtu = profileItem.GetProtocolExtra().WgMtu is null or <= 0 ? Global.TunMtus.First() : profileItem.GetProtocolExtra().WgMtu, + }); - if (profileItem.Id.IsNullOrEmpty()) + if (profileItem.Password.IsNullOrEmpty()) { return -1; } @@ -815,14 +819,13 @@ public static class ConfigHandler profileItem.CoreType = ECoreType.sing_box; profileItem.Address = profileItem.Address.TrimEx(); - profileItem.Id = profileItem.Id.TrimEx(); - profileItem.Security = profileItem.Security.TrimEx(); + profileItem.Password = profileItem.Password.TrimEx(); profileItem.Network = string.Empty; if (profileItem.StreamSecurity.IsNullOrEmpty()) { profileItem.StreamSecurity = Global.StreamSecurity; } - if (profileItem.Id.IsNullOrEmpty()) + if (profileItem.Password.IsNullOrEmpty()) { return -1; } @@ -860,7 +863,7 @@ public static class ConfigHandler Remarks = t.Remarks, Address = t.Address, Port = t.Port, - Security = t.Security, + //Security = t.Security, Network = t.Network, StreamSecurity = t.StreamSecurity, Delay = t33?.Delay ?? 0, @@ -959,26 +962,25 @@ public static class ConfigHandler profileItem.ConfigType = EConfigType.VLESS; profileItem.Address = profileItem.Address.TrimEx(); - profileItem.Id = profileItem.Id.TrimEx(); - profileItem.Security = profileItem.Security.TrimEx(); + profileItem.Password = profileItem.Password.TrimEx(); profileItem.Network = profileItem.Network.TrimEx(); profileItem.HeaderType = profileItem.HeaderType.TrimEx(); profileItem.RequestHost = profileItem.RequestHost.TrimEx(); profileItem.Path = profileItem.Path.TrimEx(); profileItem.StreamSecurity = profileItem.StreamSecurity.TrimEx(); - if (!Global.Flows.Contains(profileItem.Flow)) + var vlessEncryption = profileItem.GetProtocolExtra().VlessEncryption?.TrimEx(); + var flow = profileItem.GetProtocolExtra().Flow?.TrimEx() ?? string.Empty; + profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with { - profileItem.Flow = Global.Flows.First(); - } - if (profileItem.Id.IsNullOrEmpty()) + VlessEncryption = vlessEncryption.IsNullOrEmpty() ? Global.None : vlessEncryption, + Flow = Global.Flows.Contains(flow) ? flow : Global.Flows.First(), + }); + + if (profileItem.Password.IsNullOrEmpty()) { return -1; } - if (profileItem.Security.IsNullOrEmpty()) - { - profileItem.Security = Global.None; - } await AddServerCommon(config, profileItem, toFile); @@ -1033,7 +1035,7 @@ public static class ConfigHandler /// 0 if successful public static async Task AddServerCommon(Config config, ProfileItem profileItem, bool toFile = true) { - profileItem.ConfigVersion = 2; + profileItem.ConfigVersion = 3; if (profileItem.StreamSecurity.IsNotEmpty()) { @@ -1077,42 +1079,12 @@ public static class ConfigHandler if (toFile) { + profileItem.SetProtocolExtra(); await SQLiteHelper.Instance.ReplaceAsync(profileItem); } 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 @@ -1128,17 +1100,23 @@ public static class ConfigHandler return false; } + var oProtocolExtra = o.GetProtocolExtra(); + var nProtocolExtra = n.GetProtocolExtra(); + return o.ConfigType == n.ConfigType && AreEqual(o.Address, n.Address) && o.Port == n.Port - && AreEqual(o.Id, n.Id) - && AreEqual(o.Security, n.Security) + && AreEqual(o.Password, n.Password) + && AreEqual(oProtocolExtra.VlessEncryption, nProtocolExtra.VlessEncryption) + && AreEqual(oProtocolExtra.SsMethod, nProtocolExtra.SsMethod) + && AreEqual(oProtocolExtra.VmessSecurity, nProtocolExtra.VmessSecurity) && AreEqual(o.Network, n.Network) && AreEqual(o.HeaderType, n.HeaderType) && AreEqual(o.RequestHost, n.RequestHost) && AreEqual(o.Path, n.Path) && (o.ConfigType == EConfigType.Trojan || o.StreamSecurity == n.StreamSecurity) - && AreEqual(o.Flow, n.Flow) + && AreEqual(oProtocolExtra.Flow, nProtocolExtra.Flow) + && AreEqual(oProtocolExtra.SalamanderPass, nProtocolExtra.SalamanderPass) && AreEqual(o.Sni, n.Sni) && AreEqual(o.Alpn, n.Alpn) && AreEqual(o.Fingerprint, n.Fingerprint) @@ -1199,7 +1177,7 @@ public static class ConfigHandler var indexId = Utils.GetGuid(false); var childProfileIndexId = Utils.List2String(selecteds.Select(p => p.IndexId).ToList()); - var remark = subId.IsNullOrEmpty() ? string.Empty : $"{(await AppManager.Instance.GetSubItem(subId)).Remarks} "; + var remark = subId.IsNullOrEmpty() ? string.Empty : $"{(await AppManager.Instance.GetSubItem(subId))?.Remarks} "; if (coreType == ECoreType.Xray) { remark += multipleLoad switch @@ -1233,13 +1211,12 @@ public static class ConfigHandler { profile.Subid = subId; } - var profileGroup = new ProfileGroupItem + var extraItem = new ProtocolExtraItem { - ChildItems = childProfileIndexId, - MultipleLoad = multipleLoad, - IndexId = indexId, + ChildItems = childProfileIndexId, MultipleLoad = multipleLoad, }; - var ret = await AddGroupServerCommon(config, profile, profileGroup, true); + profile.SetProtocolExtra(extraItem); + var ret = await AddServerCommon(config, profile, true); result.Success = ret == 0; result.Data = indexId; return result; @@ -1261,7 +1238,7 @@ public static class ConfigHandler var tun2SocksAddress = node.Address; if (node.ConfigType.IsGroupType()) { - var lstAddresses = (await ProfileGroupItemManager.GetAllChildDomainAddresses(node.IndexId)).ToList(); + var lstAddresses = (await GroupProfileManager.GetAllChildDomainAddresses(node)).ToList(); if (lstAddresses.Count > 0) { tun2SocksAddress = Utils.List2String(lstAddresses); diff --git a/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs index f098b6a4..d94cbea0 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/AnytlsFmt.cs @@ -20,7 +20,7 @@ public class AnytlsFmt : BaseFmt Port = parsedUrl.Port, }; var rawUserInfo = Utils.UrlDecode(parsedUrl.UserInfo); - item.Id = rawUserInfo; + item.Password = rawUserInfo; var query = Utils.ParseQueryString(parsedUrl.Query); ResolveUriQuery(query, ref item); @@ -39,7 +39,7 @@ public class AnytlsFmt : BaseFmt { remark = "#" + Utils.UrlEncode(item.Remarks); } - var pw = item.Id; + var pw = item.Password; var dicQuery = new Dictionary(); ToUriQuery(item, Global.None, ref dicQuery); diff --git a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs index 8bd3c3de..bfafce13 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs @@ -21,11 +21,6 @@ public class BaseFmt protected static int ToUriQuery(ProfileItem item, string? securityDef, ref Dictionary dicQuery) { - if (item.Flow.IsNotEmpty()) - { - dicQuery.Add("flow", item.Flow); - } - if (item.StreamSecurity.IsNotEmpty()) { dicQuery.Add("security", item.StreamSecurity); @@ -208,7 +203,6 @@ public class BaseFmt 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"); diff --git a/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs b/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs index d92bd0c8..401beda7 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs @@ -19,16 +19,19 @@ public class Hysteria2Fmt : BaseFmt item.Address = url.IdnHost; item.Port = url.Port; item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped); - item.Id = Utils.UrlDecode(url.UserInfo); + item.Password = Utils.UrlDecode(url.UserInfo); var query = Utils.ParseQueryString(url.Query); ResolveUriQuery(query, ref item); - item.Path = GetQueryDecoded(query, "obfs-password"); - item.Ports = GetQueryDecoded(query, "mport"); if (item.CertSha.IsNullOrEmpty()) { item.CertSha = GetQueryDecoded(query, "pinSHA256"); } + item.SetProtocolExtra(item.GetProtocolExtra() with + { + Ports = GetQueryDecoded(query, "mport"), + SalamanderPass = GetQueryDecoded(query, "obfs-password"), + }); return item; } @@ -49,15 +52,16 @@ public class Hysteria2Fmt : BaseFmt } var dicQuery = new Dictionary(); ToUriQueryLite(item, ref dicQuery); + var protocolExtraItem = item.GetProtocolExtra(); - if (item.Path.IsNotEmpty()) + if (!protocolExtraItem.SalamanderPass.IsNullOrEmpty()) { dicQuery.Add("obfs", "salamander"); - dicQuery.Add("obfs-password", Utils.UrlEncode(item.Path)); + dicQuery.Add("obfs-password", Utils.UrlEncode(protocolExtraItem.SalamanderPass)); } - if (item.Ports.IsNotEmpty()) + if (!protocolExtraItem.Ports.IsNullOrEmpty()) { - dicQuery.Add("mport", Utils.UrlEncode(item.Ports.Replace(':', '-'))); + dicQuery.Add("mport", Utils.UrlEncode(protocolExtraItem.Ports.Replace(':', '-'))); } if (!item.CertSha.IsNullOrEmpty()) { @@ -70,7 +74,7 @@ public class Hysteria2Fmt : BaseFmt dicQuery.Add("pinSHA256", Utils.UrlEncode(sha)); } - return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Id, dicQuery, remark); + return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Password, dicQuery, remark); } public static ProfileItem? ResolveFull2(string strData, string? subRemarks) diff --git a/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs index 62f9e120..5b30fa46 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/ShadowsocksFmt.cs @@ -12,7 +12,8 @@ public class ShadowsocksFmt : BaseFmt { return null; } - if (item.Address.Length == 0 || item.Port == 0 || item.Security.Length == 0 || item.Id.Length == 0) + + if (item.Address.Length == 0 || item.Port == 0 || item.GetProtocolExtra().SsMethod.IsNullOrEmpty() || item.Password.Length == 0) { return null; } @@ -40,7 +41,7 @@ public class ShadowsocksFmt : BaseFmt // item.port); //url = Utile.Base64Encode(url); //new Sip002 - var pw = Utils.Base64Encode($"{item.Security}:{item.Id}", true); + var pw = Utils.Base64Encode($"{item.GetProtocolExtra().SsMethod}:{item.Password}", true); // plugin var plugin = string.Empty; @@ -136,8 +137,8 @@ public class ShadowsocksFmt : BaseFmt { return null; } - item.Security = details.Groups["method"].Value; - item.Id = details.Groups["password"].Value; + item.SetProtocolExtra(item.GetProtocolExtra() with { SsMethod = details.Groups["method"].Value }); + item.Password = details.Groups["password"].Value; item.Address = details.Groups["hostname"].Value; item.Port = details.Groups["port"].Value.ToInt(); return item; @@ -166,8 +167,8 @@ public class ShadowsocksFmt : BaseFmt { return null; } - item.Security = userInfoParts.First(); - item.Id = Utils.UrlDecode(userInfoParts.Last()); + item.SetProtocolExtra(item.GetProtocolExtra() with { SsMethod = userInfoParts.First() }); + item.Password = Utils.UrlDecode(userInfoParts.Last()); } else { @@ -178,8 +179,8 @@ public class ShadowsocksFmt : BaseFmt { return null; } - item.Security = userInfoParts.First(); - item.Id = userInfoParts.Last(); + item.SetProtocolExtra(item.GetProtocolExtra() with { SsMethod = userInfoParts.First() }); + item.Password = userInfoParts.Last(); } var queryParameters = Utils.ParseQueryString(parsedUrl.Query); @@ -275,7 +276,6 @@ public class ShadowsocksFmt : BaseFmt } } } - return item; } @@ -300,11 +300,11 @@ public class ShadowsocksFmt : BaseFmt var ssItem = new ProfileItem() { Remarks = it.remarks, - Security = it.method, - Id = it.password, + Password = it.password, Address = it.server, Port = it.server_port.ToInt() }; + ssItem.SetProtocolExtra(new ProtocolExtraItem() { SsMethod = it.method }); lst.Add(ssItem); } return lst; diff --git a/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs index ae837793..b3a54301 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}", true); + var pw = Utils.Base64Encode($"{item.Username}:{item.Password}", true); return ToUri(EConfigType.SOCKS, item.Address, item.Port, pw, null, remark); } @@ -78,9 +78,8 @@ public class SocksFmt : BaseFmt } item.Address = arr1[1][..indexPort]; item.Port = arr1[1][(indexPort + 1)..].ToInt(); - item.Security = arr21.First(); - item.Id = arr21[1]; - + item.Username = arr21.First(); + item.Password = arr21[1]; return item; } @@ -98,15 +97,14 @@ public class SocksFmt : BaseFmt Address = parsedUrl.IdnHost, Port = parsedUrl.Port, }; - // parse base64 UserInfo var rawUserInfo = Utils.UrlDecode(parsedUrl.UserInfo); var userInfo = Utils.Base64Decode(rawUserInfo); var userInfoParts = userInfo.Split([':'], 2); if (userInfoParts.Length == 2) { - item.Security = userInfoParts.First(); - item.Id = userInfoParts[1]; + item.Username = userInfoParts.First(); + item.Password = userInfoParts[1]; } return item; diff --git a/v2rayN/ServiceLib/Handler/Fmt/TrojanFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/TrojanFmt.cs index dc4794d8..630dead1 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/TrojanFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/TrojanFmt.cs @@ -20,9 +20,10 @@ public class TrojanFmt : BaseFmt item.Address = url.IdnHost; item.Port = url.Port; item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped); - item.Id = Utils.UrlDecode(url.UserInfo); + item.Password = Utils.UrlDecode(url.UserInfo); var query = Utils.ParseQueryString(url.Query); + item.SetProtocolExtra(item.GetProtocolExtra() with { Flow = GetQueryValue(query, "flow") }); ResolveUriQuery(query, ref item); return item; @@ -40,8 +41,12 @@ public class TrojanFmt : BaseFmt remark = "#" + Utils.UrlEncode(item.Remarks); } var dicQuery = new Dictionary(); + if (!item.GetProtocolExtra().Flow.IsNullOrEmpty()) + { + dicQuery.Add("flow", item.GetProtocolExtra().Flow); + } ToUriQuery(item, null, ref dicQuery); - return ToUri(EConfigType.Trojan, item.Address, item.Port, item.Id, dicQuery, remark); + return ToUri(EConfigType.Trojan, item.Address, item.Port, item.Password, dicQuery, remark); } } diff --git a/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs index 1c5aded6..56f78ea9 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/TuicFmt.cs @@ -24,8 +24,8 @@ public class TuicFmt : BaseFmt var userInfoParts = rawUserInfo.Split(new[] { ':' }, 2); if (userInfoParts.Length == 2) { - item.Id = userInfoParts.First(); - item.Security = userInfoParts.Last(); + item.Username = userInfoParts.First(); + item.Password = userInfoParts.Last(); } var query = Utils.ParseQueryString(url.Query); @@ -53,6 +53,6 @@ public class TuicFmt : BaseFmt dicQuery.Add("congestion_control", item.HeaderType); - return ToUri(EConfigType.TUIC, item.Address, item.Port, $"{item.Id}:{item.Security}", dicQuery, remark); + return ToUri(EConfigType.TUIC, item.Address, item.Port, $"{item.Username ?? ""}:{item.Password}", dicQuery, remark); } } diff --git a/v2rayN/ServiceLib/Handler/Fmt/VLESSFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/VLESSFmt.cs index 3048b51c..a3463bcd 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/VLESSFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/VLESSFmt.cs @@ -9,7 +9,6 @@ public class VLESSFmt : BaseFmt ProfileItem item = new() { ConfigType = EConfigType.VLESS, - Security = Global.None }; var url = Utils.TryUri(str); @@ -21,10 +20,14 @@ public class VLESSFmt : BaseFmt item.Address = url.IdnHost; item.Port = url.Port; item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped); - item.Id = Utils.UrlDecode(url.UserInfo); + item.Password = Utils.UrlDecode(url.UserInfo); var query = Utils.ParseQueryString(url.Query); - item.Security = GetQueryValue(query, "encryption", Global.None); + item.SetProtocolExtra(item.GetProtocolExtra() with + { + VlessEncryption = GetQueryValue(query, "encryption", Global.None), + Flow = GetQueryValue(query, "flow") + }); item.StreamSecurity = GetQueryValue(query, "security"); ResolveUriQuery(query, ref item); @@ -44,16 +47,14 @@ public class VLESSFmt : BaseFmt remark = "#" + Utils.UrlEncode(item.Remarks); } var dicQuery = new Dictionary(); - if (item.Security.IsNotEmpty()) + dicQuery.Add("encryption", + !item.GetProtocolExtra().VlessEncryption.IsNullOrEmpty() ? item.GetProtocolExtra().VlessEncryption : Global.None); + if (!item.GetProtocolExtra().Flow.IsNullOrEmpty()) { - dicQuery.Add("encryption", item.Security); - } - else - { - dicQuery.Add("encryption", Global.None); + dicQuery.Add("flow", item.GetProtocolExtra().Flow); } ToUriQuery(item, Global.None, ref dicQuery); - return ToUri(EConfigType.VLESS, item.Address, item.Port, item.Id, dicQuery, remark); + return ToUri(EConfigType.VLESS, item.Address, item.Port, item.Password, dicQuery, remark); } } diff --git a/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs index e6535d6e..b8760a40 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/VmessFmt.cs @@ -23,15 +23,16 @@ public class VmessFmt : BaseFmt { return null; } + var vmessQRCode = new VmessQRCode { - v = item.ConfigVersion, + v = 2, ps = item.Remarks.TrimEx(), add = item.Address, port = item.Port, - id = item.Id, - aid = item.AlterId, - scy = item.Security, + id = item.Password, + aid = int.TryParse(item.GetProtocolExtra()?.AlterId, out var result) ? result : 0, + scy = item.GetProtocolExtra().VmessSecurity ?? "", net = item.Network, type = item.HeaderType, host = item.RequestHost, @@ -71,15 +72,16 @@ public class VmessFmt : BaseFmt item.Network = Global.DefaultNetwork; item.HeaderType = Global.None; - item.ConfigVersion = vmessQRCode.v; + //item.ConfigVersion = vmessQRCode.v; item.Remarks = Utils.ToString(vmessQRCode.ps); item.Address = Utils.ToString(vmessQRCode.add); item.Port = vmessQRCode.port; - item.Id = Utils.ToString(vmessQRCode.id); - item.AlterId = vmessQRCode.aid; - item.Security = Utils.ToString(vmessQRCode.scy); - - item.Security = vmessQRCode.scy.IsNotEmpty() ? vmessQRCode.scy : Global.DefaultSecurity; + item.Password = Utils.ToString(vmessQRCode.id); + item.SetProtocolExtra(new ProtocolExtraItem + { + AlterId = vmessQRCode.aid.ToString(), + VmessSecurity = vmessQRCode.scy.IsNullOrEmpty() ? Global.DefaultSecurity : vmessQRCode.scy, + }); if (vmessQRCode.net.IsNotEmpty()) { item.Network = vmessQRCode.net; @@ -105,7 +107,6 @@ public class VmessFmt : BaseFmt var item = new ProfileItem { ConfigType = EConfigType.VMess, - Security = "auto" }; var url = Utils.TryUri(str); @@ -117,7 +118,12 @@ public class VmessFmt : BaseFmt item.Address = url.IdnHost; item.Port = url.Port; item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped); - item.Id = Utils.UrlDecode(url.UserInfo); + item.Password = Utils.UrlDecode(url.UserInfo); + + item.SetProtocolExtra(new ProtocolExtraItem + { + VmessSecurity = "auto", + }); var query = Utils.ParseQueryString(url.Query); ResolveUriQuery(query, ref item); diff --git a/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs index 6ceb945d..73cb6a85 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs @@ -20,14 +20,17 @@ public class WireguardFmt : BaseFmt item.Address = url.IdnHost; item.Port = url.Port; item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped); - item.Id = Utils.UrlDecode(url.UserInfo); + item.Password = Utils.UrlDecode(url.UserInfo); var query = Utils.ParseQueryString(url.Query); - item.PublicKey = GetQueryDecoded(query, "publickey"); - item.Path = GetQueryDecoded(query, "reserved"); - item.RequestHost = GetQueryDecoded(query, "address"); - item.ShortId = GetQueryDecoded(query, "mtu"); + item.SetProtocolExtra(item.GetProtocolExtra() with + { + WgPublicKey = GetQueryDecoded(query, "publickey"), + WgReserved = GetQueryDecoded(query, "reserved"), + WgInterfaceAddress = GetQueryDecoded(query, "address"), + WgMtu = int.TryParse(GetQueryDecoded(query, "mtu"), out var mtuVal) ? mtuVal : 1280, + }); return item; } @@ -46,22 +49,19 @@ public class WireguardFmt : BaseFmt } var dicQuery = new Dictionary(); - if (item.PublicKey.IsNotEmpty()) + if (!item.GetProtocolExtra().WgPublicKey.IsNullOrEmpty()) { - dicQuery.Add("publickey", Utils.UrlEncode(item.PublicKey)); + dicQuery.Add("publickey", Utils.UrlEncode(item.GetProtocolExtra().WgPublicKey)); } - if (item.Path.IsNotEmpty()) + if (!item.GetProtocolExtra().WgReserved.IsNullOrEmpty()) { - dicQuery.Add("reserved", Utils.UrlEncode(item.Path)); + dicQuery.Add("reserved", Utils.UrlEncode(item.GetProtocolExtra().WgReserved)); } - if (item.RequestHost.IsNotEmpty()) + if (!item.GetProtocolExtra().WgInterfaceAddress.IsNullOrEmpty()) { - dicQuery.Add("address", Utils.UrlEncode(item.RequestHost)); + dicQuery.Add("address", Utils.UrlEncode(item.GetProtocolExtra().WgInterfaceAddress)); } - if (item.ShortId.IsNotEmpty()) - { - dicQuery.Add("mtu", Utils.UrlEncode(item.ShortId)); - } - return ToUri(EConfigType.WireGuard, item.Address, item.Port, item.Id, dicQuery, remark); + dicQuery.Add("mtu", Utils.UrlEncode(item.GetProtocolExtra().WgMtu > 0 ? item.GetProtocolExtra().WgMtu.ToString() : "1280")); + return ToUri(EConfigType.WireGuard, item.Address, item.Port, item.Password, dicQuery, remark); } } diff --git a/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs b/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs index 965335cc..ee3cd5a0 100644 --- a/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs +++ b/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs @@ -128,23 +128,25 @@ public class ActionPrecheckManager } } + var protocolExtra = item.GetProtocolExtra(); + switch (item.ConfigType) { case EConfigType.VMess: - if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) + if (item.Password.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Password)) { - errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + errors.Add(string.Format(ResUI.InvalidProperty, "Password")); } break; case EConfigType.VLESS: - if (item.Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(item.Id) && item.Id.Length > 30)) + if (item.Password.IsNullOrEmpty() || (!Utils.IsGuidByParse(item.Password) && item.Password.Length > 30)) { - errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + errors.Add(string.Format(ResUI.InvalidProperty, "Password")); } - if (!Global.Flows.Contains(item.Flow)) + if (!Global.Flows.Contains(protocolExtra.Flow ?? string.Empty)) { errors.Add(string.Format(ResUI.InvalidProperty, "Flow")); } @@ -152,14 +154,14 @@ public class ActionPrecheckManager break; case EConfigType.Shadowsocks: - if (item.Id.IsNullOrEmpty()) + if (item.Password.IsNullOrEmpty()) { - errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + errors.Add(string.Format(ResUI.InvalidProperty, "Password")); } - if (string.IsNullOrEmpty(item.Security) || !Global.SsSecuritiesInSingbox.Contains(item.Security)) + if (string.IsNullOrEmpty(protocolExtra.SsMethod) || !Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod)) { - errors.Add(string.Format(ResUI.InvalidProperty, "Security")); + errors.Add(string.Format(ResUI.InvalidProperty, "SsMethod")); } break; @@ -202,37 +204,22 @@ public class ActionPrecheckManager { var errors = new List(); - 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); + var hasCycle = await GroupProfileManager.HasCycle(item.IndexId, item.GetProtocolExtra()); if (hasCycle) { errors.Add(string.Format(ResUI.GroupSelfReference, item.Remarks)); return errors; } - var childIds = new List(); - var subItems = await ProfileGroupItemManager.GetSubChildProfileItems(group); - childIds.AddRangeSafe(subItems.Select(p => p.IndexId)); - childIds.AddRangeSafe(Utils.String2List(group.ChildItems)); + var (childItems, _) = await GroupProfileManager.GetChildProfileItems(item); - foreach (var child in childIds) + foreach (var childItem in childItems) { 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)); + childErrors.Add(string.Format(ResUI.NodeTagNotExist, "")); continue; } diff --git a/v2rayN/ServiceLib/Manager/AppManager.cs b/v2rayN/ServiceLib/Manager/AppManager.cs index 6c20022a..576af0d0 100644 --- a/v2rayN/ServiceLib/Manager/AppManager.cs +++ b/v2rayN/ServiceLib/Manager/AppManager.cs @@ -81,7 +81,9 @@ public sealed class AppManager SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); +#pragma warning disable CS0618 SQLiteHelper.Instance.CreateTable(); +#pragma warning restore CS0618 return true; } @@ -94,6 +96,11 @@ public sealed class AppManager _ = StatePort; _ = StatePort2; + Task.Run(async () => + { + await MigrateProfileExtra(); + }).Wait(); + return true; } @@ -225,15 +232,6 @@ 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(); @@ -264,6 +262,119 @@ public sealed class AppManager return await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(it => it.CoreType == eCoreType); } + public async Task MigrateProfileExtra() + { +#pragma warning disable CS0618 + var list = await SQLiteHelper.Instance.TableAsync().ToListAsync(); + var groupItems = new ConcurrentDictionary(list.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!)); + + const int pageSize = 500; + var offset = 0; + + while (true) + { + var sql = $"SELECT * FROM ProfileItem WHERE ConfigVersion < 3 LIMIT {pageSize} OFFSET {offset}"; + var batch = await SQLiteHelper.Instance.QueryAsync(sql); + if (batch is null || batch.Count == 0) + { + break; + } + + var batchSuccessCount = 0; + foreach (var item in batch) + { + try + { + var extra = item.GetProtocolExtra(); + + if (item.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + extra = extra with { GroupType = nameof(item.ConfigType) }; + groupItems.TryGetValue(item.IndexId, out var groupItem); + if (groupItem != null && !groupItem.NotHasChild()) + { + extra = extra with + { + ChildItems = groupItem.ChildItems, + SubChildItems = groupItem.SubChildItems, + Filter = groupItem.Filter, + MultipleLoad = groupItem.MultipleLoad, + }; + } + } + + switch (item.ConfigType) + { + case EConfigType.Shadowsocks: + extra = extra with { SsMethod = item.Security.NullIfEmpty() }; + break; + case EConfigType.VMess: + extra = extra with + { + AlterId = item.AlterId.ToString(), + VmessSecurity = item.Security.NullIfEmpty(), + }; + break; + case EConfigType.VLESS: + extra = extra with + { + Flow = item.Flow.NullIfEmpty(), + VlessEncryption = item.Security, + }; + break; + case EConfigType.Hysteria2: + extra = extra with + { + SalamanderPass = item.Path.NullIfEmpty(), + Ports = item.Ports.NullIfEmpty(), + UpMbps = _config.HysteriaItem.UpMbps, + DownMbps = _config.HysteriaItem.DownMbps, + HopInterval = _config.HysteriaItem.HopInterval.ToString(), + }; + break; + case EConfigType.TUIC: + item.Username = item.Id; + item.Id = item.Security; + item.Password = item.Security; + break; + case EConfigType.HTTP: + case EConfigType.SOCKS: + item.Username = item.Security; + break; + case EConfigType.WireGuard: + extra = extra with + { + WgPublicKey = item.PublicKey.NullIfEmpty(), + WgInterfaceAddress = item.RequestHost.NullIfEmpty(), + WgReserved = item.Path.NullIfEmpty(), + WgMtu = int.TryParse(item.ShortId, out var mtu) ? mtu : 1280 + }; + break; + } + + item.SetProtocolExtra(extra); + + item.Password = item.Id; + + item.ConfigVersion = 3; + await SQLiteHelper.Instance.UpdateAsync(item); + batchSuccessCount++; + } + catch (Exception ex) + { + Logging.SaveLog($"MigrateProfileExtra Error: {ex}"); + } + } + + // Only increment offset by the number of failed items that remain in the result set + // Successfully updated items are automatically excluded from future queries due to ConfigVersion = 3 + offset += batch.Count - batchSuccessCount; + } + + //await ProfileGroupItemManager.Instance.ClearAll(); +#pragma warning restore CS0618 + } + #endregion SqliteHelper #region Core Type diff --git a/v2rayN/ServiceLib/Manager/GroupProfileManager.cs b/v2rayN/ServiceLib/Manager/GroupProfileManager.cs new file mode 100644 index 00000000..3c9d766e --- /dev/null +++ b/v2rayN/ServiceLib/Manager/GroupProfileManager.cs @@ -0,0 +1,180 @@ +namespace ServiceLib.Manager; + +public class GroupProfileManager +{ + public static async Task HasCycle(ProfileItem item) + { + return await HasCycle(item.IndexId, item.GetProtocolExtra()); + } + + public static async Task HasCycle(string? indexId, ProtocolExtraItem? extraInfo) + { + return await HasCycle(indexId, extraInfo, new HashSet(), new HashSet()); + } + + public static async Task HasCycle(string? indexId, ProtocolExtraItem? extraInfo, HashSet visited, HashSet stack) + { + if (indexId.IsNullOrEmpty() || extraInfo == null) + { + return false; + } + + if (stack.Contains(indexId)) + { + return true; + } + + if (visited.Contains(indexId)) + { + return false; + } + + visited.Add(indexId); + stack.Add(indexId); + + try + { + if (extraInfo.GroupType.IsNullOrEmpty()) + { + return false; + } + + if (extraInfo.ChildItems.IsNullOrEmpty()) + { + return false; + } + + var childIds = Utils.String2List(extraInfo.ChildItems) + ?.Where(p => !string.IsNullOrEmpty(p)) + .ToList(); + if (childIds == null) + { + return false; + } + + foreach (var child in childIds) + { + var childItem = await AppManager.Instance.GetProfileItem(child); + if (await HasCycle(child, childItem?.GetProtocolExtra(), visited, stack)) + { + return true; + } + } + + return false; + } + finally + { + stack.Remove(indexId); + } + } + + public static async Task<(List Items, ProtocolExtraItem? Extra)> GetChildProfileItems(ProfileItem profileItem) + { + var protocolExtra = profileItem?.GetProtocolExtra(); + return (await GetChildProfileItemsByProtocolExtra(protocolExtra), protocolExtra); + } + + public static async Task> GetChildProfileItemsByProtocolExtra(ProtocolExtraItem? protocolExtra) + { + if (protocolExtra == null) + { + return new(); + } + var items = await GetSelectedChildProfileItems(protocolExtra); + var subItems = await GetSubChildProfileItems(protocolExtra); + items.AddRange(subItems); + + return items; + } + + public static async Task> GetSelectedChildProfileItems(ProtocolExtraItem? extra) + { + if (extra == null || extra.ChildItems.IsNullOrEmpty()) + { + return new(); + } + var childProfiles = (await Task.WhenAll( + (Utils.String2List(extra.ChildItems) ?? new()) + .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(ProtocolExtraItem? extra) + { + if (extra == null || extra.SubChildItems.IsNullOrEmpty()) + { + return new(); + } + var childProfiles = await AppManager.Instance.ProfileItems(extra.SubChildItems ?? string.Empty); + + return childProfiles?.Where(p => + p != null && + p.IsValid() && + !p.ConfigType.IsComplexType() && + (extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter)) + ) + .ToList() ?? new(); + } + + public static async Task> GetAllChildDomainAddresses(ProfileItem profileItem) + { + var childAddresses = new HashSet(); + var (childItems, _) = await GetChildProfileItems(profileItem); + foreach (var child in childItems) + { + if (!child.IsComplex()) + { + childAddresses.Add(child.Address); + } + else if (child.ConfigType.IsGroupType()) + { + var subAddresses = await GetAllChildDomainAddresses(child); + foreach (var addr in subAddresses) + { + childAddresses.Add(addr); + } + } + } + return childAddresses; + } + + public static async Task> GetAllChildEchQuerySni(ProfileItem profileItem) + { + var childAddresses = new HashSet(); + var (childItems, _) = await GetChildProfileItems(profileItem); + foreach (var childNode in childItems) + { + if (!childNode.IsComplex() && !childNode.EchConfigList.IsNullOrEmpty()) + { + if (childNode.StreamSecurity == Global.StreamSecurity + && childNode.EchConfigList?.Contains("://") == true) + { + var idx = childNode.EchConfigList.IndexOf('+'); + childAddresses.Add(idx > 0 ? childNode.EchConfigList[..idx] : childNode.Sni); + } + else + { + childAddresses.Add(childNode.Sni); + } + } + else if (childNode.ConfigType.IsGroupType()) + { + var subAddresses = await GetAllChildDomainAddresses(childNode); + foreach (var addr in subAddresses) + { + childAddresses.Add(addr); + } + } + } + return childAddresses; + } +} diff --git a/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs deleted file mode 100644 index 041b1c78..00000000 --- a/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs +++ /dev/null @@ -1,388 +0,0 @@ -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 = new List(); - items.AddRange(await GetSubChildProfileItems(profileGroupItem)); - items.AddRange(await GetChildProfileItems(profileGroupItem)); - - 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; - } - - public static async Task> GetAllChildEchQuerySni(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); - foreach (var childNode in subItems) - { - if (childNode.EchConfigList.IsNullOrEmpty()) - { - continue; - } - if (childNode.StreamSecurity == Global.StreamSecurity - && childNode.EchConfigList?.Contains("://") == true) - { - var idx = childNode.EchConfigList.IndexOf('+'); - childAddresses.Add(idx > 0 ? childNode.EchConfigList[..idx] : childNode.Sni); - } - else - { - childAddresses.Add(childNode.Sni); - } - } - } - - 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() && !childNode.EchConfigList.IsNullOrEmpty()) - { - if (childNode.StreamSecurity == Global.StreamSecurity - && childNode.EchConfigList?.Contains("://") == true) - { - var idx = childNode.EchConfigList.IndexOf('+'); - childAddresses.Add(idx > 0 ? childNode.EchConfigList[..idx] : childNode.Sni); - } - else - { - childAddresses.Add(childNode.Sni); - } - } - 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/Models/ProfileGroupItem.cs b/v2rayN/ServiceLib/Models/ProfileGroupItem.cs index 12c0f899..94a9aad2 100644 --- a/v2rayN/ServiceLib/Models/ProfileGroupItem.cs +++ b/v2rayN/ServiceLib/Models/ProfileGroupItem.cs @@ -1,5 +1,6 @@ namespace ServiceLib.Models; +[Obsolete("Use ProtocolExtraItem instead.")] [Serializable] public class ProfileGroupItem { diff --git a/v2rayN/ServiceLib/Models/ProfileItem.cs b/v2rayN/ServiceLib/Models/ProfileItem.cs index b5424265..a43b6d18 100644 --- a/v2rayN/ServiceLib/Models/ProfileItem.cs +++ b/v2rayN/ServiceLib/Models/ProfileItem.cs @@ -3,16 +3,17 @@ namespace ServiceLib.Models; [Serializable] public class ProfileItem : ReactiveObject { + private ProtocolExtraItem? _protocolExtraCache; + public ProfileItem() { IndexId = string.Empty; ConfigType = EConfigType.VMess; - ConfigVersion = 2; + ConfigVersion = 3; Address = string.Empty; Port = 0; - Id = string.Empty; - AlterId = 0; - Security = string.Empty; + Password = string.Empty; + Username = string.Empty; Network = string.Empty; Remarks = string.Empty; HeaderType = string.Empty; @@ -21,7 +22,6 @@ public class ProfileItem : ReactiveObject StreamSecurity = string.Empty; AllowInsecure = string.Empty; Subid = string.Empty; - Flow = string.Empty; } #region function @@ -81,7 +81,7 @@ public class ProfileItem : ReactiveObject switch (ConfigType) { case EConfigType.VMess: - if (Id.IsNullOrEmpty() || !Utils.IsGuidByParse(Id)) + if (Password.IsNullOrEmpty() || !Utils.IsGuidByParse(Password)) { return false; } @@ -89,12 +89,12 @@ public class ProfileItem : ReactiveObject break; case EConfigType.VLESS: - if (Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(Id) && Id.Length > 30)) + if (Password.IsNullOrEmpty() || (!Utils.IsGuidByParse(Password) && Password.Length > 30)) { return false; } - if (!Global.Flows.Contains(Flow)) + if (!Global.Flows.Contains(GetProtocolExtra().Flow ?? string.Empty)) { return false; } @@ -102,12 +102,13 @@ public class ProfileItem : ReactiveObject break; case EConfigType.Shadowsocks: - if (Id.IsNullOrEmpty()) + if (Password.IsNullOrEmpty()) { return false; } - if (string.IsNullOrEmpty(Security) || !Global.SsSecuritiesInSingbox.Contains(Security)) + if (string.IsNullOrEmpty(GetProtocolExtra().SsMethod) + || !Global.SsSecuritiesInSingbox.Contains(GetProtocolExtra().SsMethod)) { return false; } @@ -125,6 +126,22 @@ public class ProfileItem : ReactiveObject return true; } + public void SetProtocolExtra(ProtocolExtraItem extraItem) + { + _protocolExtraCache = extraItem; + ProtoExtra = JsonUtils.Serialize(extraItem, false); + } + + public void SetProtocolExtra() + { + ProtoExtra = JsonUtils.Serialize(_protocolExtraCache, false); + } + + public ProtocolExtraItem GetProtocolExtra() + { + return _protocolExtraCache ??= JsonUtils.Deserialize(ProtoExtra) ?? new ProtocolExtraItem(); + } + #endregion function [PrimaryKey] @@ -134,10 +151,8 @@ public class ProfileItem : ReactiveObject public int ConfigVersion { get; set; } public string Address { get; set; } public int Port { get; set; } - public string Ports { get; set; } - public string Id { get; set; } - public int AlterId { get; set; } - public string Security { get; set; } + public string Password { get; set; } + public string Username { get; set; } public string Network { get; set; } public string Remarks { get; set; } public string HeaderType { get; set; } @@ -147,7 +162,6 @@ public class ProfileItem : ReactiveObject public string AllowInsecure { get; set; } public string Subid { get; set; } public bool IsSub { get; set; } = true; - public string Flow { get; set; } public string Sni { get; set; } public string Alpn { get; set; } = string.Empty; public ECoreType? CoreType { get; set; } @@ -164,4 +178,21 @@ public class ProfileItem : ReactiveObject public string CertSha { get; set; } public string EchConfigList { get; set; } public string EchForceQuery { get; set; } + + public string ProtoExtra { get; set; } + + [Obsolete("Use ProtocolExtraItem.Ports instead.")] + public string Ports { get; set; } + + [Obsolete("Use ProtocolExtraItem.AlterId instead.")] + public int AlterId { get; set; } + + [Obsolete("Use ProtocolExtraItem.Flow instead.")] + public string Flow { get; set; } + + [Obsolete("Use ProfileItem.Password instead.")] + public string Id { get; set; } + + [Obsolete("Use ProtocolExtraItem.xxx instead.")] + public string Security { get; set; } } diff --git a/v2rayN/ServiceLib/Models/ProtocolExtraItem.cs b/v2rayN/ServiceLib/Models/ProtocolExtraItem.cs new file mode 100644 index 00000000..a768ba80 --- /dev/null +++ b/v2rayN/ServiceLib/Models/ProtocolExtraItem.cs @@ -0,0 +1,38 @@ +namespace ServiceLib.Models; + +public record ProtocolExtraItem +{ + // vmess + public string? AlterId { get; init; } + public string? VmessSecurity { get; init; } + + // vless + public string? Flow { get; init; } + public string? VlessEncryption { get; init; } + //public string? VisionSeed { get; init; } + + // shadowsocks + //public string? PluginArgs { get; init; } + public string? SsMethod { get; init; } + + // wireguard + public string? WgPublicKey { get; init; } + public string? WgPresharedKey { get; init; } + public string? WgInterfaceAddress { get; init; } + public string? WgReserved { get; init; } + public int? WgMtu { get; init; } + + // hysteria2 + public string? SalamanderPass { get; init; } + public int? UpMbps { get; init; } + public int? DownMbps { get; init; } + public string? Ports { get; init; } + public string? HopInterval { get; init; } + + // group profile + public string? GroupType { get; init; } + public string? ChildItems { get; init; } + public string? SubChildItems { get; init; } + public string? Filter { get; init; } + public EMultipleLoad? MultipleLoad { get; init; } +} diff --git a/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayN/ServiceLib/Models/V2rayConfig.cs index c752168a..34456a8e 100644 --- a/v2rayN/ServiceLib/Models/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/V2rayConfig.cs @@ -473,7 +473,7 @@ public class HysteriaSettings4Ray public class HysteriaUdpHop4Ray { public string? ports { get; set; } - public int? interval { get; set; } + public string? interval { get; set; } } public class FinalMask4Ray diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index fd3b2d9f..46de8983 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -2997,6 +2997,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Port hopping interval 的本地化字符串。 + /// + public static string TbHopInt7 { + get { + return ResourceManager.GetString("TbHopInt7", resourceCulture); + } + } + /// /// 查找类似 UUID(id) 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 16fe442f..fdcffe9b 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1665,4 +1665,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. + + Port hopping interval + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index a114ac21..5db657ca 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1662,4 +1662,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. + + Port hopping interval + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index 21e4e68d..9e5c9c4d 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1665,4 +1665,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. + + Port hopping interval + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 91095de4..dfd744a3 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1665,4 +1665,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. + + Port hopping interval + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index adc9cc98..67cffea7 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1665,4 +1665,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. + + Port hopping interval + \ 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 b1675d48..f17495fd 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1662,4 +1662,7 @@ 当未选择或 "AsIs" 时,由远程服务器端 DNS 解析;否则,使用内部 DNS 模块解析。 + + 端口跳跃间隔 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 73a7dfe5..160340d1 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1662,4 +1662,7 @@ If unset or "AsIs", DNS resolution is performed by the remote server's DNS; otherwise, the internal DNS module is used. + + Port hopping interval + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index 7684cbd1..6e4cca95 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -242,7 +242,7 @@ public partial class CoreConfigSingboxService } else if (node?.ConfigType.IsGroupType() == true) { - var queryServerNames = (await ProfileGroupItemManager.GetAllChildEchQuerySni(node.IndexId)).ToList(); + var queryServerNames = (await GroupProfileManager.GetAllChildEchQuerySni(node)).ToList(); if (queryServerNames.Count > 0) { singboxConfig.dns.rules.Add(new() diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs index e2c0d502..178350b3 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs @@ -6,6 +6,7 @@ public partial class CoreConfigSingboxService { try { + var protocolExtra = node.GetProtocolExtra(); outbound.server = node.Address; outbound.server_port = node.Port; outbound.type = Global.ProtocolTypes[node.ConfigType]; @@ -14,11 +15,11 @@ public partial class CoreConfigSingboxService { case EConfigType.VMess: { - outbound.uuid = node.Id; - outbound.alter_id = node.AlterId; - if (Global.VmessSecurities.Contains(node.Security)) + outbound.uuid = node.Password; + outbound.alter_id = int.TryParse(protocolExtra.AlterId, out var result) ? result : 0; + if (Global.VmessSecurities.Contains(protocolExtra.VmessSecurity)) { - outbound.security = node.Security; + outbound.security = protocolExtra.VmessSecurity; } else { @@ -31,8 +32,9 @@ public partial class CoreConfigSingboxService } case EConfigType.Shadowsocks: { - outbound.method = AppManager.Instance.GetShadowsocksSecurities(node).Contains(node.Security) ? node.Security : Global.None; - outbound.password = node.Id; + outbound.method = AppManager.Instance.GetShadowsocksSecurities(node).Contains(protocolExtra.SsMethod) + ? protocolExtra.SsMethod : Global.None; + outbound.password = node.Password; if (node.Network == nameof(ETransport.tcp) && node.HeaderType == Global.TcpHeaderHttp) { @@ -88,37 +90,37 @@ public partial class CoreConfigSingboxService case EConfigType.SOCKS: { outbound.version = "5"; - if (node.Security.IsNotEmpty() - && node.Id.IsNotEmpty()) + if (node.Username.IsNotEmpty() + && node.Password.IsNotEmpty()) { - outbound.username = node.Security; - outbound.password = node.Id; + outbound.username = node.Username; + outbound.password = node.Password; } break; } case EConfigType.HTTP: { - if (node.Security.IsNotEmpty() - && node.Id.IsNotEmpty()) + if (node.Username.IsNotEmpty() + && node.Password.IsNotEmpty()) { - outbound.username = node.Security; - outbound.password = node.Id; + outbound.username = node.Username; + outbound.password = node.Password; } break; } case EConfigType.VLESS: { - outbound.uuid = node.Id; + outbound.uuid = node.Password; outbound.packet_encoding = "xudp"; - if (node.Flow.IsNullOrEmpty()) + if (!protocolExtra.Flow.IsNullOrEmpty()) { - await GenOutboundMux(node, outbound); + outbound.flow = protocolExtra.Flow; } else { - outbound.flow = node.Flow; + await GenOutboundMux(node, outbound); } await GenOutboundTransport(node, outbound); @@ -126,7 +128,7 @@ public partial class CoreConfigSingboxService } case EConfigType.Trojan: { - outbound.password = node.Id; + outbound.password = node.Password; await GenOutboundMux(node, outbound); await GenOutboundTransport(node, outbound); @@ -134,23 +136,28 @@ public partial class CoreConfigSingboxService } case EConfigType.Hysteria2: { - outbound.password = node.Id; + outbound.password = node.Password; - if (node.Path.IsNotEmpty()) + if (!protocolExtra.SalamanderPass.IsNullOrEmpty()) { outbound.obfs = new() { type = "salamander", - password = node.Path.TrimEx(), + password = protocolExtra.SalamanderPass.TrimEx(), }; } - outbound.up_mbps = _config.HysteriaItem.UpMbps > 0 ? _config.HysteriaItem.UpMbps : null; - outbound.down_mbps = _config.HysteriaItem.DownMbps > 0 ? _config.HysteriaItem.DownMbps : null; - if (node.Ports.IsNotEmpty() && (node.Ports.Contains(':') || node.Ports.Contains('-') || node.Ports.Contains(','))) + outbound.up_mbps = protocolExtra?.UpMbps is { } su and >= 0 + ? su + : _config.HysteriaItem.UpMbps; + outbound.down_mbps = protocolExtra?.DownMbps is { } sd and >= 0 + ? sd + : _config.HysteriaItem.DownMbps; + var ports = protocolExtra?.Ports?.IsNullOrEmpty() == false ? protocolExtra.Ports : null; + if ((!ports.IsNullOrEmpty()) && (ports.Contains(':') || ports.Contains('-') || ports.Contains(','))) { outbound.server_port = null; - outbound.server_ports = node.Ports.Split(',') + outbound.server_ports = ports.Split(',') .Select(p => p.Trim()) .Where(p => p.IsNotEmpty()) .Select(p => @@ -159,21 +166,38 @@ public partial class CoreConfigSingboxService return port.Contains(':') ? port : $"{port}:{port}"; }) .ToList(); - outbound.hop_interval = _config.HysteriaItem.HopInterval > 0 ? $"{_config.HysteriaItem.HopInterval}s" : null; + outbound.hop_interval = _config.HysteriaItem.HopInterval >= 5 + ? $"{_config.HysteriaItem.HopInterval}s" + : $"{Global.Hysteria2DefaultHopInt}s"; + if (int.TryParse(protocolExtra.HopInterval, out var hiResult)) + { + outbound.hop_interval = hiResult >= 5 ? $"{hiResult}s" : outbound.hop_interval; + } + else if (protocolExtra.HopInterval?.Contains('-') ?? false) + { + // may be a range like 5-10 + var parts = protocolExtra.HopInterval.Split('-'); + if (parts.Length == 2 && int.TryParse(parts[0], out var hiL) && + int.TryParse(parts[0], out var hiH)) + { + var hi = (hiL + hiH) / 2; + outbound.hop_interval = hi >= 5 ? $"{hi}s" : outbound.hop_interval; + } + } } break; } case EConfigType.TUIC: { - outbound.uuid = node.Id; - outbound.password = node.Security; + outbound.uuid = node.Username; + outbound.password = node.Password; outbound.congestion_control = node.HeaderType; break; } case EConfigType.Anytls: { - outbound.password = node.Id; + outbound.password = node.Password; break; } } @@ -191,7 +215,9 @@ public partial class CoreConfigSingboxService { try { - endpoint.address = Utils.String2List(node.RequestHost); + var protocolExtra = node.GetProtocolExtra(); + + endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress); endpoint.type = Global.ProtocolTypes[node.ConfigType]; switch (node.ConfigType) @@ -200,16 +226,17 @@ public partial class CoreConfigSingboxService { var peer = new Peer4Sbox { - public_key = node.PublicKey, - reserved = Utils.String2List(node.Path)?.Select(int.Parse).ToList(), + public_key = protocolExtra.WgPublicKey, + pre_shared_key = protocolExtra.WgPresharedKey, + reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(int.Parse).ToList(), address = node.Address, port = node.Port, // TODO default ["0.0.0.0/0", "::/0"] allowed_ips = new() { "0.0.0.0/0", "::/0" }, }; - endpoint.private_key = node.Id; - endpoint.mtu = node.ShortId.IsNullOrEmpty() ? Global.TunMtus.First() : node.ShortId.ToInt(); - endpoint.peers = new() { peer }; + endpoint.private_key = node.Password; + endpoint.mtu = protocolExtra.WgMtu > 0 ? protocolExtra.WgMtu : Global.TunMtus.First(); + endpoint.peers = [peer]; break; } } @@ -453,13 +480,13 @@ public partial class CoreConfigSingboxService { return -1; } - var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId); + var hasCycle = await GroupProfileManager.HasCycle(node); if (hasCycle) { return -1; } - var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + var (childProfiles, profileExtraItem) = await GroupProfileManager.GetChildProfileItems(node); if (childProfiles.Count <= 0) { return -1; @@ -467,13 +494,14 @@ public partial class CoreConfigSingboxService switch (node.ConfigType) { case EConfigType.PolicyGroup: + var multipleLoad = profileExtraItem?.MultipleLoad ?? EMultipleLoad.LeastPing; if (ignoreOriginChain) { - await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName); + await GenOutboundsList(childProfiles, singboxConfig, multipleLoad, baseTagName); } else { - await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName); + await GenOutboundsListWithChain(childProfiles, singboxConfig, multipleLoad, baseTagName); } break; @@ -585,7 +613,7 @@ public partial class CoreConfigSingboxService if (node.ConfigType.IsGroupType()) { - var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + var (childProfiles, profileExtraItem) = await GroupProfileManager.GetChildProfileItems(node); if (childProfiles.Count <= 0) { continue; @@ -594,7 +622,8 @@ public partial class CoreConfigSingboxService var ret = node.ConfigType switch { EConfigType.PolicyGroup => - await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName), + await GenOutboundsListWithChain(childProfiles, singboxConfig, + profileExtraItem?.MultipleLoad ?? EMultipleLoad.LeastPing, childBaseTagName), EConfigType.ProxyChain => await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName), _ => throw new NotImplementedException() @@ -763,7 +792,7 @@ public partial class CoreConfigSingboxService if (node.ConfigType.IsGroupType()) { - var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + var (childProfiles, profileExtraItem) = await GroupProfileManager.GetChildProfileItems(node); if (childProfiles.Count <= 0) { continue; @@ -772,7 +801,7 @@ public partial class CoreConfigSingboxService var ret = node.ConfigType switch { EConfigType.PolicyGroup => - await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName), + await GenOutboundsList(childProfiles, singboxConfig, profileExtraItem?.MultipleLoad ?? EMultipleLoad.LeastPing, childBaseTagName), EConfigType.ProxyChain => await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName), _ => throw new NotImplementedException() diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index d3575685..7f9c26ca 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -6,6 +6,7 @@ public partial class CoreConfigV2rayService { try { + var protocolExtra = node.GetProtocolExtra(); var muxEnabled = node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled; switch (node.ConfigType) { @@ -35,12 +36,12 @@ public partial class CoreConfigV2rayService usersItem = vnextItem.users.First(); } - usersItem.id = node.Id; - usersItem.alterId = node.AlterId; + usersItem.id = node.Password; + usersItem.alterId = int.TryParse(protocolExtra?.AlterId, out var result) ? result : 0; usersItem.email = Global.UserEMail; - if (Global.VmessSecurities.Contains(node.Security)) + if (Global.VmessSecurities.Contains(protocolExtra.VmessSecurity)) { - usersItem.security = node.Security; + usersItem.security = protocolExtra.VmessSecurity; } else { @@ -66,8 +67,9 @@ public partial class CoreConfigV2rayService } serversItem.address = node.Address; serversItem.port = node.Port; - serversItem.password = node.Id; - serversItem.method = AppManager.Instance.GetShadowsocksSecurities(node).Contains(node.Security) ? node.Security : "none"; + serversItem.password = node.Password; + serversItem.method = AppManager.Instance.GetShadowsocksSecurities(node).Contains(protocolExtra.SsMethod) + ? protocolExtra.SsMethod : "none"; serversItem.ota = false; serversItem.level = 1; @@ -95,13 +97,13 @@ public partial class CoreConfigV2rayService serversItem.method = null; serversItem.password = null; - if (node.Security.IsNotEmpty() - && node.Id.IsNotEmpty()) + if (node.Username.IsNotEmpty() + && node.Password.IsNotEmpty()) { SocksUsersItem4Ray socksUsersItem = new() { - user = node.Security, - pass = node.Id, + user = node.Username ?? "", + pass = node.Password, level = 1 }; @@ -138,17 +140,16 @@ public partial class CoreConfigV2rayService { usersItem = vnextItem.users.First(); } - usersItem.id = node.Id; + usersItem.id = node.Password; usersItem.email = Global.UserEMail; - usersItem.encryption = node.Security; + usersItem.encryption = protocolExtra.VlessEncryption; - if (node.Flow.IsNullOrEmpty()) + if (!protocolExtra.Flow.IsNullOrEmpty()) { - await GenOutboundMux(node, outbound, muxEnabled, muxEnabled); + usersItem.flow = protocolExtra.Flow; } else { - usersItem.flow = node.Flow; await GenOutboundMux(node, outbound, false, muxEnabled); } outbound.settings.servers = null; @@ -168,7 +169,7 @@ public partial class CoreConfigV2rayService } serversItem.address = node.Address; serversItem.port = node.Port; - serversItem.password = node.Id; + serversItem.password = node.Password; serversItem.ota = false; serversItem.level = 1; @@ -199,16 +200,16 @@ public partial class CoreConfigV2rayService } var peer = new WireguardPeer4Ray { - publicKey = node.PublicKey, + publicKey = protocolExtra.WgPublicKey ?? "", endpoint = address + ":" + node.Port.ToString() }; var setting = new Outboundsettings4Ray { - address = Utils.String2List(node.RequestHost), - secretKey = node.Id, - reserved = Utils.String2List(node.Path)?.Select(int.Parse).ToList(), - mtu = node.ShortId.IsNullOrEmpty() ? Global.TunMtus.First() : node.ShortId.ToInt(), - peers = new List { peer } + address = Utils.String2List(protocolExtra.WgInterfaceAddress), + secretKey = node.Password, + reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(int.Parse).ToList(), + mtu = protocolExtra.WgMtu > 0 ? protocolExtra.WgMtu : Global.TunMtus.First(), + peers = [peer] }; outbound.settings = setting; outbound.settings.vnext = null; @@ -509,28 +510,38 @@ public partial class CoreConfigV2rayService break; case "hysteria": + var protocolExtra = node.GetProtocolExtra(); + var ports = protocolExtra?.Ports; + int? upMbps = protocolExtra?.UpMbps is { } su and >= 0 + ? su + : _config.HysteriaItem.UpMbps; + int? downMbps = protocolExtra?.DownMbps is { } sd and >= 0 + ? sd + : _config.HysteriaItem.UpMbps; + var hopInterval = !protocolExtra.HopInterval.IsNullOrEmpty() + ? protocolExtra.HopInterval + : (_config.HysteriaItem.HopInterval >= 5 + ? _config.HysteriaItem.HopInterval + : Global.Hysteria2DefaultHopInt).ToString(); HysteriaUdpHop4Ray? udpHop = null; - if (node.Ports.IsNotEmpty() && - (node.Ports.Contains(':') || node.Ports.Contains('-') || node.Ports.Contains(','))) + if (!ports.IsNullOrEmpty() && + (ports.Contains(':') || ports.Contains('-') || ports.Contains(','))) { - udpHop = new() + udpHop = new HysteriaUdpHop4Ray { - ports = node.Ports.Replace(':', '-'), - interval = _config.HysteriaItem.HopInterval > 0 - ? _config.HysteriaItem.HopInterval - : null, + ports = ports.Replace(':', '-'), + interval = hopInterval, }; } - HysteriaSettings4Ray hysteriaSettings = new() + streamSettings.hysteriaSettings = new() { version = 2, - auth = node.Id, - up = _config.HysteriaItem.UpMbps > 0 ? $"{_config.HysteriaItem.UpMbps}mbps" : null, - down = _config.HysteriaItem.DownMbps > 0 ? $"{_config.HysteriaItem.DownMbps}mbps" : null, + auth = node.Password, + up = upMbps > 0 ? $"{upMbps}mbps" : null, + down = downMbps > 0 ? $"{downMbps}mbps" : null, udphop = udpHop, }; - streamSettings.hysteriaSettings = hysteriaSettings; - if (node.Path.IsNotEmpty()) + if (!protocolExtra.SalamanderPass.IsNullOrEmpty()) { streamSettings.finalmask ??= new(); streamSettings.finalmask.udp = @@ -538,7 +549,7 @@ public partial class CoreConfigV2rayService new Mask4Ray { type = "salamander", - settings = new MaskSettings4Ray { password = node.Path.TrimEx(), } + settings = new MaskSettings4Ray { password = protocolExtra.SalamanderPass.TrimEx(), } } ]; } @@ -592,13 +603,13 @@ public partial class CoreConfigV2rayService { return -1; } - var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId); + var hasCycle = await GroupProfileManager.HasCycle(node); if (hasCycle) { return -1; } - var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + var (childProfiles, profileExtraItem) = await GroupProfileManager.GetChildProfileItems(node); if (childProfiles.Count <= 0) { return -1; @@ -627,8 +638,9 @@ public partial class CoreConfigV2rayService //add balancers if (node.ConfigType == EConfigType.PolicyGroup) { - await GenObservatory(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName); - await GenBalancer(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName); + var multipleLoad = profileExtraItem?.MultipleLoad ?? EMultipleLoad.LeastPing; + await GenObservatory(v2rayConfig, multipleLoad, baseTagName); + await GenBalancer(v2rayConfig, multipleLoad, baseTagName); } } catch (Exception ex) @@ -737,7 +749,7 @@ public partial class CoreConfigV2rayService if (node.ConfigType.IsGroupType()) { - var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + var (childProfiles, _) = await GroupProfileManager.GetChildProfileItems(node); if (childProfiles.Count <= 0) { continue; @@ -891,7 +903,7 @@ public partial class CoreConfigV2rayService if (node.ConfigType.IsGroupType()) { - var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + var (childProfiles, _) = await GroupProfileManager.GetChildProfileItems(node); if (childProfiles.Count <= 0) { continue; diff --git a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs index 5b0778a5..c19d218b 100644 --- a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs @@ -79,8 +79,8 @@ public class AddGroupServerViewModel : MyReactiveObject public async Task Init() { - ProfileGroupItemManager.Instance.TryGet(SelectedSource.IndexId, out var profileGroup); - PolicyGroupType = (profileGroup?.MultipleLoad ?? EMultipleLoad.LeastPing) switch + var protocolExtra = SelectedSource.GetProtocolExtra(); + PolicyGroupType = (protocolExtra?.MultipleLoad ?? EMultipleLoad.LeastPing) switch { EMultipleLoad.LeastPing => ResUI.TbLeastPing, EMultipleLoad.Fallback => ResUI.TbFallback, @@ -93,22 +93,18 @@ public class AddGroupServerViewModel : MyReactiveObject 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; + SelectedSubItem = SubItems.FirstOrDefault(s => s.Id == protocolExtra?.SubChildItems); + Filter = protocolExtra?.Filter; - var childItemMulti = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource?.IndexId); - if (childItemMulti != null) + var childIndexIds = Utils.String2List(protocolExtra?.ChildItems) ?? []; + foreach (var item in childIndexIds) { - var childIndexIds = Utils.String2List(childItemMulti.ChildItems) ?? []; - foreach (var item in childIndexIds) + var child = await AppManager.Instance.GetProfileItem(item); + if (child == null) { - var child = await AppManager.Instance.GetProfileItem(item); - if (child == null) - { - continue; - } - ChildItemsObs.Add(child); + continue; } + ChildItemsObs.Add(child); } } @@ -205,38 +201,32 @@ public class AddGroupServerViewModel : MyReactiveObject { return; } - var childIndexIds = new List(); - foreach (var item in ChildItemsObs) + + SelectedSource.SetProtocolExtra(SelectedSource.GetProtocolExtra() with { - if (item.IndexId.IsNullOrEmpty()) + ChildItems = + Utils.List2String(ChildItemsObs.Where(s => !s.IndexId.IsNullOrEmpty()).Select(s => s.IndexId).ToList()), + MultipleLoad = PolicyGroupType switch { - 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, - }; + 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, + }, + SubChildItems = SelectedSubItem?.Id, + Filter = Filter, + }); - profileGroup.SubChildItems = SelectedSubItem?.Id; - profileGroup.Filter = Filter; - - var hasCycle = ProfileGroupItemManager.HasCycle(profileGroup.IndexId); + var hasCycle = await GroupProfileManager.HasCycle(SelectedSource.IndexId, SelectedSource.GetProtocolExtra()); if (hasCycle) { NoticeManager.Instance.Enqueue(string.Format(ResUI.GroupSelfReference, remarks)); return; } - if (await ConfigHandler.AddGroupServerCommon(_config, SelectedSource, profileGroup, true) == 0) + if (await ConfigHandler.AddServerCommon(_config, SelectedSource) == 0) { NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); _updateView?.Invoke(EViewAction.CloseWindow, null); diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index 775274d7..914705c8 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -17,6 +17,47 @@ public class AddServerViewModel : MyReactiveObject [Reactive] public string CertSha { get; set; } + [Reactive] + public string SalamanderPass { get; set; } + + [Reactive] + public int AlterId { get; set; } + + [Reactive] + public string Ports { get; set; } + + [Reactive] + public int UpMbps { get; set; } + + [Reactive] + public int DownMbps { get; set; } + + [Reactive] + public string HopInterval { get; set; } + + [Reactive] + public string Flow { get; set; } + + [Reactive] + public string VmessSecurity { get; set; } + + [Reactive] + public string VlessEncryption { get; set; } + + [Reactive] + public string SsMethod { get; set; } + + [Reactive] + public string WgPublicKey { get; set; } + //[Reactive] + //public string WgPresharedKey { get; set; } + [Reactive] + public string WgInterfaceAddress { get; set; } + [Reactive] + public string WgReserved { get; set; } + [Reactive] + public int WgMtu { get; set; } + public ReactiveCommand FetchCertCmd { get; } public ReactiveCommand FetchCertChainCmd { get; } public ReactiveCommand SaveCmd { get; } @@ -63,6 +104,22 @@ public class AddServerViewModel : MyReactiveObject CoreType = SelectedSource?.CoreType?.ToString(); Cert = SelectedSource?.Cert?.ToString() ?? string.Empty; CertSha = SelectedSource?.CertSha?.ToString() ?? string.Empty; + + var protocolExtra = SelectedSource?.GetProtocolExtra(); + Ports = protocolExtra?.Ports ?? string.Empty; + AlterId = int.TryParse(protocolExtra?.AlterId, out var result) ? result : 0; + Flow = protocolExtra?.Flow ?? string.Empty; + SalamanderPass = protocolExtra?.SalamanderPass ?? string.Empty; + UpMbps = protocolExtra?.UpMbps ?? _config.HysteriaItem.UpMbps; + DownMbps = protocolExtra?.DownMbps ?? _config.HysteriaItem.DownMbps; + HopInterval = protocolExtra?.HopInterval.IsNullOrEmpty() ?? true ? Global.Hysteria2DefaultHopInt.ToString() : protocolExtra.HopInterval; + VmessSecurity = protocolExtra?.VmessSecurity?.IsNullOrEmpty() == false ? protocolExtra.VmessSecurity : Global.DefaultSecurity; + VlessEncryption = protocolExtra?.VlessEncryption.IsNullOrEmpty() == false ? protocolExtra.VlessEncryption : Global.None; + SsMethod = protocolExtra?.SsMethod ?? string.Empty; + WgPublicKey = protocolExtra?.WgPublicKey ?? string.Empty; + WgInterfaceAddress = protocolExtra?.WgInterfaceAddress ?? string.Empty; + WgReserved = protocolExtra?.WgReserved ?? string.Empty; + WgMtu = protocolExtra?.WgMtu ?? 1280; } private async Task SaveServerAsync() @@ -87,12 +144,12 @@ public class AddServerViewModel : MyReactiveObject } if (SelectedSource.ConfigType == EConfigType.Shadowsocks) { - if (SelectedSource.Id.IsNullOrEmpty()) + if (SelectedSource.Password.IsNullOrEmpty()) { NoticeManager.Instance.Enqueue(ResUI.FillPassword); return; } - if (SelectedSource.Security.IsNullOrEmpty()) + if (SsMethod.IsNullOrEmpty()) { NoticeManager.Instance.Enqueue(ResUI.PleaseSelectEncryption); return; @@ -100,7 +157,7 @@ public class AddServerViewModel : MyReactiveObject } if (SelectedSource.ConfigType is not EConfigType.SOCKS and not EConfigType.HTTP) { - if (SelectedSource.Id.IsNullOrEmpty()) + if (SelectedSource.Password.IsNullOrEmpty()) { NoticeManager.Instance.Enqueue(ResUI.FillUUID); return; @@ -109,6 +166,23 @@ public class AddServerViewModel : MyReactiveObject SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); SelectedSource.Cert = Cert.IsNullOrEmpty() ? string.Empty : Cert; SelectedSource.CertSha = CertSha.IsNullOrEmpty() ? string.Empty : CertSha; + SelectedSource.SetProtocolExtra(SelectedSource.GetProtocolExtra() with + { + Ports = Ports.NullIfEmpty(), + AlterId = AlterId > 0 ? AlterId.ToString() : null, + Flow = Flow.NullIfEmpty(), + SalamanderPass = SalamanderPass.NullIfEmpty(), + UpMbps = UpMbps >= 0 ? UpMbps : null, + DownMbps = DownMbps >= 0 ? DownMbps : null, + HopInterval = HopInterval.NullIfEmpty(), + VmessSecurity = VmessSecurity.NullIfEmpty(), + VlessEncryption = VlessEncryption.NullIfEmpty(), + SsMethod = SsMethod.NullIfEmpty(), + WgPublicKey = WgPublicKey.NullIfEmpty(), + WgInterfaceAddress = WgInterfaceAddress.NullIfEmpty(), + WgReserved = WgReserved.NullIfEmpty(), + WgMtu = WgMtu >= 576 ? WgMtu : null, + }); if (await ConfigHandler.AddServer(_config, SelectedSource) == 0) { diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index a09ad4de..8f5e9029 100644 --- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -259,7 +259,6 @@ public class MainWindowViewModel : MyReactiveObject 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); diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs index 7301882c..ca065d5d 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs @@ -209,7 +209,7 @@ public class ProfilesSelectViewModel : MyReactiveObject Remarks = t.Remarks, Address = t.Address, Port = t.Port, - Security = t.Security, + //Security = t.Security, Network = t.Network, StreamSecurity = t.StreamSecurity, Subid = t.Subid, diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 0e141b74..7bd4cb98 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -446,7 +446,7 @@ public class ProfilesViewModel : MyReactiveObject Remarks = t.Remarks, Address = t.Address, Port = t.Port, - Security = t.Security, + //Security = t.Security, Network = t.Network, StreamSecurity = t.StreamSecurity, Subid = t.Subid, diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml index 19c62868..356698fb 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml @@ -360,7 +360,7 @@ Grid.Row="2" ColumnDefinitions="300,Auto,Auto" IsVisible="False" - RowDefinitions="Auto,Auto,Auto,Auto"> + RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto"> + + + + + + + + + case EConfigType.VMess: gridVMess.IsVisible = true; cmbSecurity.ItemsSource = Global.VmessSecurities; - if (profileItem.Security.IsNullOrEmpty()) - { - profileItem.Security = Global.DefaultSecurity; - } break; case EConfigType.Shadowsocks: @@ -59,10 +55,6 @@ public partial class AddServerWindow : WindowBase gridVLESS.IsVisible = true; lstStreamSecurity.Add(Global.StreamSecurityReality); cmbFlow5.ItemsSource = Global.Flows; - if (profileItem.Security.IsNullOrEmpty()) - { - profileItem.Security = Global.None; - } break; case EConfigType.Trojan: @@ -119,59 +111,62 @@ public partial class AddServerWindow : WindowBase switch (profileItem.ConfigType) { case EConfigType.VMess: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.AlterId, v => v.txtAlterId.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.cmbSecurity.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.AlterId, v => v.txtAlterId.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.VmessSecurity, v => v.cmbSecurity.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(disposables); break; case EConfigType.Shadowsocks: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId3.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.cmbSecurity3.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId3.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SsMethod, v => v.cmbSecurity3.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled3.IsChecked).DisposeWith(disposables); break; case EConfigType.SOCKS: case EConfigType.HTTP: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId4.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.txtSecurity4.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId4.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtSecurity4.Text).DisposeWith(disposables); break; case EConfigType.VLESS: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId5.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Flow, v => v.cmbFlow5.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.txtSecurity5.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId5.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow5.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.VlessEncryption, v => v.txtSecurity5.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled5.IsChecked).DisposeWith(disposables); break; case EConfigType.Trojan: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId6.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Flow, v => v.cmbFlow6.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId6.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow6.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled6.IsChecked).DisposeWith(disposables); break; case EConfigType.Hysteria2: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId7.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Path, v => v.txtPath7.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Ports, v => v.txtPorts7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SalamanderPass, v => v.txtPath7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Ports, v => v.txtPorts7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.HopInterval, v => v.txtHopInt7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.UpMbps, v => v.txtUpMbps7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.DownMbps, v => v.txtDownMbps7.Text).DisposeWith(disposables); break; case EConfigType.TUIC: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId8.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.txtSecurity8.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId8.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity8.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.HeaderType, v => v.cmbHeaderType8.SelectedValue).DisposeWith(disposables); break; case EConfigType.WireGuard: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId9.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.PublicKey, v => v.txtPublicKey9.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Path, v => v.txtPath9.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.RequestHost, v => v.txtRequestHost9.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.ShortId, v => v.txtShortId9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.WgPublicKey, v => v.txtPublicKey9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.WgReserved, v => v.txtPath9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.WgInterfaceAddress, v => v.txtRequestHost9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.WgMtu, v => v.txtShortId9.Text).DisposeWith(disposables); break; case EConfigType.Anytls: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId10.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId10.Text).DisposeWith(disposables); break; } this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables); diff --git a/v2rayN/v2rayN/Views/AddServerWindow.xaml b/v2rayN/v2rayN/Views/AddServerWindow.xaml index 1f70489b..8c6b4a35 100644 --- a/v2rayN/v2rayN/Views/AddServerWindow.xaml +++ b/v2rayN/v2rayN/Views/AddServerWindow.xaml @@ -487,6 +487,8 @@ + + @@ -547,6 +549,47 @@ VerticalAlignment="Center" Style="{StaticResource ToolbarTextBlock}" Text="{x:Static resx:ResUI.TbPorts7Tips}" /> + + + + + + + + + vm.SelectedSource.Id, v => v.txtId.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.AlterId, v => v.txtAlterId.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.cmbSecurity.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.AlterId, v => v.txtAlterId.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.VmessSecurity, v => v.cmbSecurity.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(disposables); break; case EConfigType.Shadowsocks: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId3.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.cmbSecurity3.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId3.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SsMethod, v => v.cmbSecurity3.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled3.IsChecked).DisposeWith(disposables); break; case EConfigType.SOCKS: case EConfigType.HTTP: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId4.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.txtSecurity4.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId4.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtSecurity4.Text).DisposeWith(disposables); break; case EConfigType.VLESS: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId5.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Flow, v => v.cmbFlow5.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.txtSecurity5.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId5.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow5.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.VlessEncryption, v => v.txtSecurity5.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled5.IsChecked).DisposeWith(disposables); break; case EConfigType.Trojan: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId6.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Flow, v => v.cmbFlow6.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId6.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow6.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.MuxEnabled, v => v.togmuxEnabled6.IsChecked).DisposeWith(disposables); break; case EConfigType.Hysteria2: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId7.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Path, v => v.txtPath7.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Ports, v => v.txtPorts7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SalamanderPass, v => v.txtPath7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Ports, v => v.txtPorts7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.HopInterval, v => v.txtHopInt7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.UpMbps, v => v.txtUpMbps7.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.DownMbps, v => v.txtDownMbps7.Text).DisposeWith(disposables); break; case EConfigType.TUIC: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId8.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Security, v => v.txtSecurity8.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId8.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity8.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.HeaderType, v => v.cmbHeaderType8.Text).DisposeWith(disposables); break; case EConfigType.WireGuard: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId9.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.PublicKey, v => v.txtPublicKey9.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Path, v => v.txtPath9.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.RequestHost, v => v.txtRequestHost9.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.ShortId, v => v.txtShortId9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.WgPublicKey, v => v.txtPublicKey9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.WgReserved, v => v.txtPath9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.WgInterfaceAddress, v => v.txtRequestHost9.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.WgMtu, v => v.txtShortId9.Text).DisposeWith(disposables); break; case EConfigType.Anytls: - this.Bind(ViewModel, vm => vm.SelectedSource.Id, v => v.txtId10.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId10.Text).DisposeWith(disposables); break; } this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.Text).DisposeWith(disposables); From 7a58e783816d6de9e9429fdac621db12c742fe2d Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:45:26 +0800 Subject: [PATCH 20/24] Refactor profile migration and add group handling --- v2rayN/ServiceLib/Manager/AppManager.cs | 266 ++++++++++++++++-------- 1 file changed, 176 insertions(+), 90 deletions(-) diff --git a/v2rayN/ServiceLib/Manager/AppManager.cs b/v2rayN/ServiceLib/Manager/AppManager.cs index 576af0d0..83633c0f 100644 --- a/v2rayN/ServiceLib/Manager/AppManager.cs +++ b/v2rayN/ServiceLib/Manager/AppManager.cs @@ -264,107 +264,26 @@ public sealed class AppManager public async Task MigrateProfileExtra() { -#pragma warning disable CS0618 - var list = await SQLiteHelper.Instance.TableAsync().ToListAsync(); - var groupItems = new ConcurrentDictionary(list.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!)); + await MigrateProfileExtraGroup(); - const int pageSize = 500; +#pragma warning disable CS0618 + + const int pageSize = 100; var offset = 0; while (true) { - var sql = $"SELECT * FROM ProfileItem WHERE ConfigVersion < 3 LIMIT {pageSize} OFFSET {offset}"; + var sql = $"SELECT * FROM ProfileItem " + + $"WHERE ConfigVersion < 3 " + + $"AND ConfigType NOT IN ({(int)EConfigType.PolicyGroup}, {(int)EConfigType.ProxyChain}) " + + $"LIMIT {pageSize} OFFSET {offset}"; var batch = await SQLiteHelper.Instance.QueryAsync(sql); if (batch is null || batch.Count == 0) { break; } - var batchSuccessCount = 0; - foreach (var item in batch) - { - try - { - var extra = item.GetProtocolExtra(); - - if (item.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) - { - extra = extra with { GroupType = nameof(item.ConfigType) }; - groupItems.TryGetValue(item.IndexId, out var groupItem); - if (groupItem != null && !groupItem.NotHasChild()) - { - extra = extra with - { - ChildItems = groupItem.ChildItems, - SubChildItems = groupItem.SubChildItems, - Filter = groupItem.Filter, - MultipleLoad = groupItem.MultipleLoad, - }; - } - } - - switch (item.ConfigType) - { - case EConfigType.Shadowsocks: - extra = extra with { SsMethod = item.Security.NullIfEmpty() }; - break; - case EConfigType.VMess: - extra = extra with - { - AlterId = item.AlterId.ToString(), - VmessSecurity = item.Security.NullIfEmpty(), - }; - break; - case EConfigType.VLESS: - extra = extra with - { - Flow = item.Flow.NullIfEmpty(), - VlessEncryption = item.Security, - }; - break; - case EConfigType.Hysteria2: - extra = extra with - { - SalamanderPass = item.Path.NullIfEmpty(), - Ports = item.Ports.NullIfEmpty(), - UpMbps = _config.HysteriaItem.UpMbps, - DownMbps = _config.HysteriaItem.DownMbps, - HopInterval = _config.HysteriaItem.HopInterval.ToString(), - }; - break; - case EConfigType.TUIC: - item.Username = item.Id; - item.Id = item.Security; - item.Password = item.Security; - break; - case EConfigType.HTTP: - case EConfigType.SOCKS: - item.Username = item.Security; - break; - case EConfigType.WireGuard: - extra = extra with - { - WgPublicKey = item.PublicKey.NullIfEmpty(), - WgInterfaceAddress = item.RequestHost.NullIfEmpty(), - WgReserved = item.Path.NullIfEmpty(), - WgMtu = int.TryParse(item.ShortId, out var mtu) ? mtu : 1280 - }; - break; - } - - item.SetProtocolExtra(extra); - - item.Password = item.Id; - - item.ConfigVersion = 3; - await SQLiteHelper.Instance.UpdateAsync(item); - batchSuccessCount++; - } - catch (Exception ex) - { - Logging.SaveLog($"MigrateProfileExtra Error: {ex}"); - } - } + var batchSuccessCount = await MigrateProfileExtraSub(batch); // Only increment offset by the number of failed items that remain in the result set // Successfully updated items are automatically excluded from future queries due to ConfigVersion = 3 @@ -375,6 +294,173 @@ public sealed class AppManager #pragma warning restore CS0618 } + private async Task MigrateProfileExtraSub(List batch) + { + var updateProfileItems = new List(); + + foreach (var item in batch) + { + try + { + var extra = item.GetProtocolExtra(); + switch (item.ConfigType) + { + case EConfigType.Shadowsocks: + extra = extra with { SsMethod = item.Security.NullIfEmpty() }; + break; + + case EConfigType.VMess: + extra = extra with + { + AlterId = item.AlterId.ToString(), + VmessSecurity = item.Security.NullIfEmpty(), + }; + break; + + case EConfigType.VLESS: + extra = extra with + { + Flow = item.Flow.NullIfEmpty(), + VlessEncryption = item.Security, + }; + break; + + case EConfigType.Hysteria2: + extra = extra with + { + SalamanderPass = item.Path.NullIfEmpty(), + Ports = item.Ports.NullIfEmpty(), + UpMbps = _config.HysteriaItem.UpMbps, + DownMbps = _config.HysteriaItem.DownMbps, + HopInterval = _config.HysteriaItem.HopInterval.ToString(), + }; + break; + + case EConfigType.TUIC: + item.Username = item.Id; + item.Id = item.Security; + item.Password = item.Security; + break; + + case EConfigType.HTTP: + case EConfigType.SOCKS: + item.Username = item.Security; + break; + + case EConfigType.WireGuard: + extra = extra with + { + WgPublicKey = item.PublicKey.NullIfEmpty(), + WgInterfaceAddress = item.RequestHost.NullIfEmpty(), + WgReserved = item.Path.NullIfEmpty(), + WgMtu = int.TryParse(item.ShortId, out var mtu) ? mtu : 1280 + }; + break; + } + + item.SetProtocolExtra(extra); + + item.Password = item.Id; + + item.ConfigVersion = 3; + + updateProfileItems.Add(item); + } + catch (Exception ex) + { + Logging.SaveLog($"MigrateProfileExtra Error: {ex}"); + } + } + + if (updateProfileItems.Count > 0) + { + try + { + var count = await SQLiteHelper.Instance.UpdateAllAsync(updateProfileItems); + return count; + } + catch (Exception ex) + { + Logging.SaveLog($"MigrateProfileExtraGroup update error: {ex}"); + return 0; + } + } + else + { + return 0; + } + } + + private async Task MigrateProfileExtraGroup() + { +#pragma warning disable CS0618 + var list = await SQLiteHelper.Instance.TableAsync().ToListAsync(); + var groupItems = new ConcurrentDictionary(list.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!)); + + var sql = $"SELECT * FROM ProfileItem WHERE ConfigVersion < 3 AND ConfigType IN ({(int)EConfigType.PolicyGroup}, {(int)EConfigType.ProxyChain})"; + var items = await SQLiteHelper.Instance.QueryAsync(sql); + + if (items is null || items.Count == 0) + { + Logging.SaveLog("MigrateProfileExtraGroup: No items to migrate."); + return true; + } + + Logging.SaveLog($"MigrateProfileExtraGroup: Found {items.Count} group items to migrate."); + + var updateProfileItems = new List(); + + foreach (var item in items) + { + try + { + var extra = item.GetProtocolExtra(); + + extra = extra with { GroupType = nameof(item.ConfigType) }; + groupItems.TryGetValue(item.IndexId, out var groupItem); + if (groupItem != null && !groupItem.NotHasChild()) + { + extra = extra with + { + ChildItems = groupItem.ChildItems, + SubChildItems = groupItem.SubChildItems, + Filter = groupItem.Filter, + MultipleLoad = groupItem.MultipleLoad, + }; + } + + item.SetProtocolExtra(extra); + + item.ConfigVersion = 3; + updateProfileItems.Add(item); + } + catch (Exception ex) + { + Logging.SaveLog($"MigrateProfileExtraGroup item error [{item.IndexId}]: {ex}"); + } + } + + if (updateProfileItems.Count > 0) + { + try + { + var count = await SQLiteHelper.Instance.UpdateAllAsync(updateProfileItems); + Logging.SaveLog($"MigrateProfileExtraGroup: Successfully updated {updateProfileItems.Count} items."); + return updateProfileItems.Count == count; + } + catch (Exception ex) + { + Logging.SaveLog($"MigrateProfileExtraGroup update error: {ex}"); + return false; + } + } + + return true; + + //await ProfileGroupItemManager.Instance.ClearAll(); +#pragma warning restore CS0618 + } + #endregion SqliteHelper #region Core Type From 6167624443914924a501cc7007b665040f36b136 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Fri, 6 Feb 2026 13:50:47 +0800 Subject: [PATCH 21/24] Rename ProfileItems to ProfileModels and refactor --- v2rayN/ServiceLib/Handler/ConfigHandler.cs | 7 +++--- v2rayN/ServiceLib/Helper/DownloaderHelper.cs | 2 +- v2rayN/ServiceLib/Manager/AppManager.cs | 13 ++++++++--- v2rayN/ServiceLib/Models/ConfigItems.cs | 2 +- v2rayN/ServiceLib/Models/ProfileItem.cs | 16 ++++++------- v2rayN/ServiceLib/Models/ProfileItemModel.cs | 23 +++++++++++++++++-- .../ViewModels/AddServerViewModel.cs | 3 +++ .../ViewModels/ProfilesSelectViewModel.cs | 2 +- .../ViewModels/ProfilesViewModel.cs | 4 ++-- .../ViewModels/StatusBarViewModel.cs | 4 ++-- 10 files changed, 53 insertions(+), 23 deletions(-) diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index 0ea515ec..37176608 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -844,7 +844,7 @@ public static class ConfigHandler /// 0 if successful, -1 if failed public static async Task SortServers(Config config, string subId, string colName, bool asc) { - var lstModel = await AppManager.Instance.ProfileItems(subId, ""); + var lstModel = await AppManager.Instance.ProfileModels(subId, ""); if (lstModel.Count <= 0) { return -1; @@ -1213,7 +1213,8 @@ public static class ConfigHandler } var extraItem = new ProtocolExtraItem { - ChildItems = childProfileIndexId, MultipleLoad = multipleLoad, + ChildItems = childProfileIndexId, + MultipleLoad = multipleLoad, }; profile.SetProtocolExtra(extraItem); var ret = await AddServerCommon(config, profile, true); @@ -1277,7 +1278,7 @@ public static class ConfigHandler /// Number of removed servers or -1 if failed public static async Task RemoveInvalidServerResult(Config config, string subid) { - var lstModel = await AppManager.Instance.ProfileItems(subid, ""); + var lstModel = await AppManager.Instance.ProfileModels(subid, ""); if (lstModel is { Count: <= 0 }) { return -1; diff --git a/v2rayN/ServiceLib/Helper/DownloaderHelper.cs b/v2rayN/ServiceLib/Helper/DownloaderHelper.cs index 907d416f..f4a7ae7f 100644 --- a/v2rayN/ServiceLib/Helper/DownloaderHelper.cs +++ b/v2rayN/ServiceLib/Helper/DownloaderHelper.cs @@ -85,7 +85,7 @@ public class DownloaderHelper { maxSpeed = value.BytesPerSecondSpeed; } - + var ts = DateTime.Now - lastUpdateTime; if (ts.TotalMilliseconds >= 1000) { diff --git a/v2rayN/ServiceLib/Manager/AppManager.cs b/v2rayN/ServiceLib/Manager/AppManager.cs index 83633c0f..0e471568 100644 --- a/v2rayN/ServiceLib/Manager/AppManager.cs +++ b/v2rayN/ServiceLib/Manager/AppManager.cs @@ -191,10 +191,17 @@ public sealed class AppManager return (await ProfileItems(subid))?.Select(t => t.IndexId)?.ToList(); } - public async Task?> ProfileItems(string subid, string filter) + public async Task?> ProfileModels(string subid, string filter) { - var sql = @$"select a.* - ,b.remarks subRemarks + var sql = @$"select a.IndexId + ,a.ConfigType + ,a.Remarks + ,a.Address + ,a.Port + ,a.Network + ,a.StreamSecurity + ,a.Subid + ,b.remarks as subRemarks from ProfileItem a left join SubItem b on a.subid = b.id where 1=1 "; diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs index 46297e40..46caee85 100644 --- a/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/ConfigItems.cs @@ -99,7 +99,7 @@ public class UIItem public bool EnableDragDropSort { get; set; } public bool DoubleClick2Activate { get; set; } public bool AutoHideStartup { get; set; } - public bool Hide2TrayWhenClose { get; set; } + public bool Hide2TrayWhenClose { get; set; } public bool MacOSShowInDock { get; set; } public List MainColumnItem { get; set; } public List WindowSizeItem { get; set; } diff --git a/v2rayN/ServiceLib/Models/ProfileItem.cs b/v2rayN/ServiceLib/Models/ProfileItem.cs index a43b6d18..6e9d2a30 100644 --- a/v2rayN/ServiceLib/Models/ProfileItem.cs +++ b/v2rayN/ServiceLib/Models/ProfileItem.cs @@ -1,7 +1,7 @@ namespace ServiceLib.Models; [Serializable] -public class ProfileItem : ReactiveObject +public class ProfileItem { private ProtocolExtraItem? _protocolExtraCache; @@ -10,6 +10,7 @@ public class ProfileItem : ReactiveObject IndexId = string.Empty; ConfigType = EConfigType.VMess; ConfigVersion = 3; + Subid = string.Empty; Address = string.Empty; Port = 0; Password = string.Empty; @@ -21,7 +22,6 @@ public class ProfileItem : ReactiveObject Path = string.Empty; StreamSecurity = string.Empty; AllowInsecure = string.Empty; - Subid = string.Empty; } #region function @@ -148,26 +148,26 @@ public class ProfileItem : ReactiveObject public string IndexId { get; set; } public EConfigType ConfigType { get; set; } + public ECoreType? CoreType { get; set; } public int ConfigVersion { get; set; } + public string Subid { get; set; } + public bool IsSub { get; set; } = true; + public int? PreSocksPort { get; set; } + public bool DisplayLog { get; set; } = true; + public string Remarks { get; set; } public string Address { get; set; } public int Port { get; set; } public string Password { get; set; } public string Username { get; set; } public string Network { get; set; } - public string Remarks { get; set; } public string HeaderType { get; set; } public string RequestHost { get; set; } public string Path { get; set; } public string StreamSecurity { get; set; } public string AllowInsecure { get; set; } - public string Subid { get; set; } - public bool IsSub { get; set; } = true; public string Sni { get; set; } public string Alpn { get; set; } = string.Empty; - public ECoreType? CoreType { get; set; } - public int? PreSocksPort { get; set; } public string Fingerprint { get; set; } - public bool DisplayLog { get; set; } = true; public string PublicKey { get; set; } public string ShortId { get; set; } public string SpiderX { get; set; } diff --git a/v2rayN/ServiceLib/Models/ProfileItemModel.cs b/v2rayN/ServiceLib/Models/ProfileItemModel.cs index b4d72119..53170f17 100644 --- a/v2rayN/ServiceLib/Models/ProfileItemModel.cs +++ b/v2rayN/ServiceLib/Models/ProfileItemModel.cs @@ -1,16 +1,24 @@ namespace ServiceLib.Models; [Serializable] -public class ProfileItemModel : ProfileItem +public class ProfileItemModel : ReactiveObject { public bool IsActive { get; set; } + public string IndexId { get; set; } + public EConfigType ConfigType { get; set; } + public string Remarks { get; set; } + public string Address { get; set; } + public int Port { get; set; } + public string Network { get; set; } + public string StreamSecurity { get; set; } + public string Subid { get; set; } public string SubRemarks { get; set; } + public int Sort { get; set; } [Reactive] public int Delay { get; set; } public decimal Speed { get; set; } - public int Sort { get; set; } [Reactive] public string DelayVal { get; set; } @@ -29,4 +37,15 @@ public class ProfileItemModel : ProfileItem [Reactive] public string TotalDown { get; set; } + + public string GetSummary() + { + var summary = $"[{ConfigType}] {Remarks}"; + if (!ConfigType.IsComplexType()) + { + summary += $"({Address}:{Port})"; + } + + return summary; + } } diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index 914705c8..ba698c20 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -49,12 +49,15 @@ public class AddServerViewModel : MyReactiveObject [Reactive] public string WgPublicKey { get; set; } + //[Reactive] //public string WgPresharedKey { get; set; } [Reactive] public string WgInterfaceAddress { get; set; } + [Reactive] public string WgReserved { get; set; } + [Reactive] public int WgMtu { get; set; } diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs index ca065d5d..ce349cb4 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs @@ -200,7 +200,7 @@ public class ProfilesSelectViewModel : MyReactiveObject private async Task?> GetProfileItemsEx(string subid, string filter) { - var lstModel = await AppManager.Instance.ProfileItems(_subIndexId, filter); + var lstModel = await AppManager.Instance.ProfileModels(_subIndexId, filter); lstModel = (from t in lstModel select new ProfileItemModel { diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 7bd4cb98..2d838189 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -56,7 +56,7 @@ public class ProfilesViewModel : MyReactiveObject public ReactiveCommand MoveUpCmd { get; } public ReactiveCommand MoveDownCmd { get; } - public ReactiveCommand MoveBottomCmd { get; } + public ReactiveCommand MoveBottomCmd { get; } public ReactiveCommand MoveToGroupCmd { get; } //servers ping @@ -428,7 +428,7 @@ public class ProfilesViewModel : MyReactiveObject private async Task?> GetProfileItemsEx(string subid, string filter) { - var lstModel = await AppManager.Instance.ProfileItems(_config.SubIndexId, filter); + var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, filter); await ConfigHandler.SetDefaultServer(_config, lstModel); diff --git a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs index b8cd142e..9269851d 100644 --- a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs @@ -303,7 +303,7 @@ public class StatusBarViewModel : MyReactiveObject private async Task RefreshServersMenu() { - var lstModel = await AppManager.Instance.ProfileItems(_config.SubIndexId, ""); + var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, ""); Servers.Clear(); if (lstModel.Count > _config.GuiItem.TrayMenuServersLimit) @@ -315,7 +315,7 @@ public class StatusBarViewModel : MyReactiveObject BlServers = true; for (var k = 0; k < lstModel.Count; k++) { - ProfileItem it = lstModel[k]; + var it = lstModel[k]; var name = it.GetSummary(); var item = new ComboItem() { ID = it.IndexId, Text = name }; From 54608ab2b91fed417a06d5f2dd5d91fa5b8747ee Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:15:21 +0800 Subject: [PATCH 22/24] Adjust GroupProfileManager --- .../ServiceLib/Manager/GroupProfileManager.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/v2rayN/ServiceLib/Manager/GroupProfileManager.cs b/v2rayN/ServiceLib/Manager/GroupProfileManager.cs index 3c9d766e..4296059d 100644 --- a/v2rayN/ServiceLib/Manager/GroupProfileManager.cs +++ b/v2rayN/ServiceLib/Manager/GroupProfileManager.cs @@ -12,7 +12,7 @@ public class GroupProfileManager return await HasCycle(indexId, extraInfo, new HashSet(), new HashSet()); } - public static async Task HasCycle(string? indexId, ProtocolExtraItem? extraInfo, HashSet visited, HashSet stack) + private static async Task HasCycle(string? indexId, ProtocolExtraItem? extraInfo, HashSet visited, HashSet stack) { if (indexId.IsNullOrEmpty() || extraInfo == null) { @@ -75,20 +75,21 @@ public class GroupProfileManager return (await GetChildProfileItemsByProtocolExtra(protocolExtra), protocolExtra); } - public static async Task> GetChildProfileItemsByProtocolExtra(ProtocolExtraItem? protocolExtra) + private static async Task> GetChildProfileItemsByProtocolExtra(ProtocolExtraItem? protocolExtra) { if (protocolExtra == null) { return new(); } - var items = await GetSelectedChildProfileItems(protocolExtra); - var subItems = await GetSubChildProfileItems(protocolExtra); - items.AddRange(subItems); + + var items = new List(); + items.AddRange(await GetSubChildProfileItems(protocolExtra)); + items.AddRange(await GetSelectedChildProfileItems(protocolExtra)); return items; } - public static async Task> GetSelectedChildProfileItems(ProtocolExtraItem? extra) + private static async Task> GetSelectedChildProfileItems(ProtocolExtraItem? extra) { if (extra == null || extra.ChildItems.IsNullOrEmpty()) { @@ -105,10 +106,10 @@ public class GroupProfileManager p.ConfigType != EConfigType.Custom ) .ToList(); - return childProfiles; + return childProfiles ?? new(); } - public static async Task> GetSubChildProfileItems(ProtocolExtraItem? extra) + private static async Task> GetSubChildProfileItems(ProtocolExtraItem? extra) { if (extra == null || extra.SubChildItems.IsNullOrEmpty()) { From 0f3a3eac02b7d263f713e26b1231c26fbadbf779 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 6 Feb 2026 06:33:58 +0000 Subject: [PATCH 23/24] Group preview (#8760) * Group Preview * Fix --- .../ServiceLib/Manager/GroupProfileManager.cs | 2 +- v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 9 ++++ v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 3 ++ v2rayN/ServiceLib/Resx/ResUI.fr.resx | 3 ++ v2rayN/ServiceLib/Resx/ResUI.hu.resx | 3 ++ v2rayN/ServiceLib/Resx/ResUI.resx | 3 ++ v2rayN/ServiceLib/Resx/ResUI.ru.resx | 3 ++ v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 3 ++ v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 3 ++ .../ViewModels/AddGroupServerViewModel.cs | 49 ++++++++++++------- .../Views/AddGroupServerWindow.axaml | 48 +++++++++++++++++- .../Views/AddGroupServerWindow.axaml.cs | 31 +++++++++++- v2rayN/v2rayN/Views/AddGroupServerWindow.xaml | 42 ++++++++++++++++ .../v2rayN/Views/AddGroupServerWindow.xaml.cs | 32 ++++++++++++ 14 files changed, 213 insertions(+), 21 deletions(-) diff --git a/v2rayN/ServiceLib/Manager/GroupProfileManager.cs b/v2rayN/ServiceLib/Manager/GroupProfileManager.cs index 4296059d..53ef0429 100644 --- a/v2rayN/ServiceLib/Manager/GroupProfileManager.cs +++ b/v2rayN/ServiceLib/Manager/GroupProfileManager.cs @@ -75,7 +75,7 @@ public class GroupProfileManager return (await GetChildProfileItemsByProtocolExtra(protocolExtra), protocolExtra); } - private static async Task> GetChildProfileItemsByProtocolExtra(ProtocolExtraItem? protocolExtra) + public static async Task> GetChildProfileItemsByProtocolExtra(ProtocolExtraItem? protocolExtra) { if (protocolExtra == null) { diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 46de8983..6f223693 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -1680,6 +1680,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Configuration item preview 的本地化字符串。 + /// + public static string menuServerListPreview { + get { + return ResourceManager.GetString("menuServerListPreview", resourceCulture); + } + } + /// /// 查找类似 Configuration 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index fdcffe9b..dbce6ac4 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1668,4 +1668,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Port hopping interval + + Configuration item preview + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index 5db657ca..005a51d5 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1665,4 +1665,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Port hopping interval + + Configuration item preview + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index 9e5c9c4d..3d169590 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1668,4 +1668,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Port hopping interval + + Configuration item preview + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index dfd744a3..041a103b 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1668,4 +1668,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Port hopping interval + + Configuration item preview + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 67cffea7..66ebef79 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1668,4 +1668,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Port hopping interval + + Configuration item preview + \ 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 f17495fd..c8936668 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1665,4 +1665,7 @@ 端口跳跃间隔 + + 子配置项预览 + \ 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 160340d1..2ee4dc4f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1665,4 +1665,7 @@ Port hopping interval + + Configuration item preview + \ No newline at end of file diff --git a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs index c19d218b..6e4d021c 100644 --- a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs @@ -27,6 +27,8 @@ public class AddGroupServerViewModel : MyReactiveObject public IObservableCollection ChildItemsObs { get; } = new ObservableCollectionExtended(); + public IObservableCollection AllProfilePreviewItemsObs { get; } = new ObservableCollectionExtended(); + //public ReactiveCommand AddCmd { get; } public ReactiveCommand RemoveCmd { get; } @@ -182,6 +184,32 @@ public class AddGroupServerViewModel : MyReactiveObject await Task.CompletedTask; } + private ProtocolExtraItem GetUpdatedProtocolExtra() + { + return SelectedSource.GetProtocolExtra() with + { + ChildItems = + Utils.List2String(ChildItemsObs.Where(s => !s.IndexId.IsNullOrEmpty()).Select(s => s.IndexId).ToList()), + 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, + }, + SubChildItems = SelectedSubItem?.Id, + Filter = Filter, + }; + } + + public async Task UpdatePreviewList() + { + AllProfilePreviewItemsObs.Clear(); + AllProfilePreviewItemsObs.AddRange(await GroupProfileManager.GetChildProfileItemsByProtocolExtra(GetUpdatedProtocolExtra())); + } + private async Task SaveServerAsync() { var remarks = SelectedSource.Remarks; @@ -202,24 +230,11 @@ public class AddGroupServerViewModel : MyReactiveObject return; } - SelectedSource.SetProtocolExtra(SelectedSource.GetProtocolExtra() with - { - ChildItems = - Utils.List2String(ChildItemsObs.Where(s => !s.IndexId.IsNullOrEmpty()).Select(s => s.IndexId).ToList()), - 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, - }, - SubChildItems = SelectedSubItem?.Id, - Filter = Filter, - }); + var protocolExtra = GetUpdatedProtocolExtra(); - var hasCycle = await GroupProfileManager.HasCycle(SelectedSource.IndexId, SelectedSource.GetProtocolExtra()); + SelectedSource.SetProtocolExtra(protocolExtra); + + var hasCycle = await GroupProfileManager.HasCycle(SelectedSource.IndexId, protocolExtra); if (hasCycle) { NoticeManager.Instance.Enqueue(string.Format(ResUI.GroupSelfReference, remarks)); diff --git a/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml index bb95588d..c036bb03 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml @@ -88,7 +88,10 @@ - + + + + + + + + + + + + + diff --git a/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml.cs index 26827350..142f9d6b 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml.cs @@ -16,6 +16,7 @@ public partial class AddGroupServerWindow : WindowBase Loaded += Window_Loaded; btnCancel.Click += (s, e) => Close(); lstChild.SelectionChanged += LstChild_SelectionChanged; + tabControl.SelectionChanged += TabControl_SelectionChanged; ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler); @@ -38,6 +39,10 @@ public partial class AddGroupServerWindow : WindowBase case EConfigType.ProxyChain: Title = ResUI.TbConfigTypeProxyChain; gridPolicyGroup.IsVisible = false; + if (tabControl.Items.Count > 0) + { + tabControl.Items.RemoveAt(0); + } break; } @@ -50,7 +55,6 @@ public partial class AddGroupServerWindow : WindowBase this.Bind(ViewModel, vm => vm.SelectedSubItem, v => v.cmbSubChildItems.SelectedItem).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Filter, v => v.txtFilter.Text).DisposeWith(disposables); - this.OneWayBind(ViewModel, vm => vm.ChildItemsObs, v => v.lstChild.ItemsSource).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables); @@ -167,4 +171,29 @@ public partial class AddGroupServerWindow : WindowBase ViewModel.SelectedChildren = lstChild.SelectedItems.Cast().ToList(); } } + + private async void TabControl_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + try + { + if (e.Source is not TabControl tc) + { + return; + } + if (!(tc.SelectedIndex == tc.Items.Count - 1 && tc.Items.Count > 0)) + { + return; + } + if (ViewModel == null) + { + return; + } + + await ViewModel.UpdatePreviewList(); + } + catch + { + // ignored + } + } } diff --git a/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml b/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml index 841b3d84..baaa0114 100644 --- a/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml +++ b/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml @@ -135,6 +135,7 @@ @@ -272,6 +273,47 @@ + + + + + + + + + + + + diff --git a/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml.cs b/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml.cs index 14f5a7bf..4b1642d2 100644 --- a/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml.cs +++ b/v2rayN/v2rayN/Views/AddGroupServerWindow.xaml.cs @@ -11,6 +11,7 @@ public partial class AddGroupServerWindow PreviewKeyDown += AddGroupServerWindow_PreviewKeyDown; lstChild.SelectionChanged += LstChild_SelectionChanged; menuSelectAllChild.Click += MenuSelectAllChild_Click; + tabControl.SelectionChanged += TabControl_SelectionChanged; ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler); @@ -33,6 +34,10 @@ public partial class AddGroupServerWindow case EConfigType.ProxyChain: Title = ResUI.TbConfigTypeProxyChain; gridPolicyGroup.Visibility = Visibility.Collapsed; + if (tabControl.Items.Count > 0) + { + tabControl.Items.RemoveAt(0); + } break; } @@ -48,6 +53,8 @@ public partial class AddGroupServerWindow this.OneWayBind(ViewModel, vm => vm.ChildItemsObs, v => v.lstChild.ItemsSource).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables); + this.OneWayBind(ViewModel, vm => vm.AllProfilePreviewItemsObs, v => v.lstPreviewChild.ItemsSource).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables); @@ -148,4 +155,29 @@ public partial class AddGroupServerWindow { lstChild.SelectAll(); } + + private async void TabControl_SelectionChanged(object? sender, System.Windows.Controls.SelectionChangedEventArgs e) + { + try + { + if (e.Source is not System.Windows.Controls.TabControl tc) + { + return; + } + if (!(tc.SelectedIndex == tc.Items.Count - 1 && tc.Items.Count > 0)) + { + return; + } + if (ViewModel == null) + { + return; + } + + await ViewModel.UpdatePreviewList(); + } + catch + { + // ignored + } + } } From b5800f7dfc50af575e3bd3581fdab5f0d5937470 Mon Sep 17 00:00:00 2001 From: JieXu Date: Sat, 7 Feb 2026 19:31:35 +0800 Subject: [PATCH 24/24] Update SingboxDnsService.cs (#8775) * Update Utils.cs * Update SingboxDnsService.cs * Update package-rhel.sh * Update package-rhel.sh * Withdraw --- package-rhel.sh | 59 +++++-------------- v2rayN/ServiceLib/Common/Utils.cs | 4 +- .../CoreConfig/Singbox/SingboxDnsService.cs | 2 +- 3 files changed, 17 insertions(+), 48 deletions(-) diff --git a/package-rhel.sh b/package-rhel.sh index 06ef786a..507e2b18 100644 --- a/package-rhel.sh +++ b/package-rhel.sh @@ -10,7 +10,7 @@ if [[ -r /etc/os-release ]]; then ;; *) echo "[ERROR] Unsupported system: $NAME ($ID)." - echo "This script only supports Red Hat Enterprise Linux/RockyLinux/AlmaLinux/CentOS or Ubuntu/Debian." + echo "This script only supports Red Hat Enterprise Linux/RockyLinux/AlmaLinux/CentOS." exit 1 ;; esac @@ -483,20 +483,11 @@ build_for_arch() { trap '[[ -n "${WORKDIR:-}" ]] && rm -rf "$WORKDIR"' RETURN # rpmbuild topdir selection - local TOPDIR SPECDIR SOURCEDIR USE_TOPDIR_DEFINE - if [[ "$ID" =~ ^(rhel|rocky|almalinux|centos)$ ]]; then - rpmdev-setuptree - TOPDIR="${HOME}/rpmbuild" - SPECDIR="${TOPDIR}/SPECS" - SOURCEDIR="${TOPDIR}/SOURCES" - USE_TOPDIR_DEFINE=0 - else - TOPDIR="${WORKDIR}/rpmbuild" - SPECDIR="${TOPDIR}/SPECS}" - SOURCEDIR="${TOPDIR}/SOURCES" - mkdir -p "${SPECDIR}" "${SOURCEDIR}" "${TOPDIR}/BUILD" "${TOPDIR}/RPMS" "${TOPDIR}/SRPMS" - USE_TOPDIR_DEFINE=1 - fi + local TOPDIR SPECDIR SOURCEDIR + rpmdev-setuptree + TOPDIR="${HOME}/rpmbuild" + SPECDIR="${TOPDIR}/SPECS" + SOURCEDIR="${TOPDIR}/SOURCES" # Stage publish content mkdir -p "$WORKDIR/$PKGROOT" @@ -533,8 +524,6 @@ build_for_arch() { download_singbox "$WORKDIR/$PKGROOT/bin/sing_box" || echo "[!] sing-box download failed (skipped)" fi download_geo_assets "$WORKDIR/$PKGROOT" || echo "[!] Geo rules download failed (skipped)" - # ---- REQUIRED: always fetch mihomo in netcore mode, per-arch ---- - # download_mihomo "$WORKDIR/$PKGROOT" || echo "[!] mihomo download failed (skipped)" fi # Tarball @@ -588,6 +577,12 @@ https://github.com/2dust/v2rayN install -dm0755 %{buildroot}/opt/v2rayN cp -a * %{buildroot}/opt/v2rayN/ +install -dm0755 %{buildroot}%{_sysconfdir}/sudoers.d +cat > %{buildroot}%{_sysconfdir}/sudoers.d/v2rayn-mihomo-deny << 'EOF' +ALL ALL=(ALL) !/home/*/.local/share/v2rayN/bin/mihomo/mihomo +EOF +chmod 0440 %{buildroot}%{_sysconfdir}/sudoers.d/v2rayn-mihomo-deny + # Launcher (prefer native ELF first, then DLL fallback) install -dm0755 %{buildroot}%{_bindir} cat > %{buildroot}%{_bindir}/v2rayn << 'EOF' @@ -641,6 +636,7 @@ fi /opt/v2rayN %{_datadir}/applications/v2rayn.desktop %{_datadir}/icons/hicolor/256x256/apps/v2rayn.png +%config(noreplace) /etc/sudoers.d/v2rayn-mihomo-deny SPEC # Autostart injection (inside %install) and %files entry @@ -680,35 +676,8 @@ SPEC sed -i "s/__VERSION__/${VERSION}/g" "$SPECFILE" sed -i "s/__PKGROOT__/${PKGROOT}/g" "$SPECFILE" - # ----- Select proper 'strip' per target arch on Ubuntu only (cross-binutils) ----- - # NOTE: We define only __strip to point to the target-arch strip. - # DO NOT override __brp_strip (it must stay the brp script path). - local STRIP_ARGS=() - if [[ "$ID" == "ubuntu" ]]; then - local STRIP_BIN="" - if [[ "$short" == "x64" ]]; then - STRIP_BIN="/usr/bin/x86_64-linux-gnu-strip" - else - STRIP_BIN="/usr/bin/aarch64-linux-gnu-strip" - fi - if [[ -x "$STRIP_BIN" ]]; then - STRIP_ARGS=( --define "__strip $STRIP_BIN" ) - fi - fi - # Build RPM for this arch (force rpm --target to match compile arch) - if [[ "$USE_TOPDIR_DEFINE" -eq 1 ]]; then - rpmbuild -ba "$SPECFILE" --define "_topdir $TOPDIR" --target "$rpm_target" "${STRIP_ARGS[@]}" - else - rpmbuild -ba "$SPECFILE" --target "$rpm_target" "${STRIP_ARGS[@]}" - fi - - # Copy temporary rpmbuild to ~/rpmbuild on Debian/Ubuntu path - if [[ "$USE_TOPDIR_DEFINE" -eq 1 ]]; then - mkdir -p "$HOME/rpmbuild" - rsync -a "$TOPDIR"/ "$HOME/rpmbuild"/ - TOPDIR="$HOME/rpmbuild" - fi + rpmbuild -ba "$SPECFILE" --target "$rpm_target" echo "Build done for $short. RPM at:" local f diff --git a/v2rayN/ServiceLib/Common/Utils.cs b/v2rayN/ServiceLib/Common/Utils.cs index c538f9c8..70365e99 100644 --- a/v2rayN/ServiceLib/Common/Utils.cs +++ b/v2rayN/ServiceLib/Common/Utils.cs @@ -339,13 +339,13 @@ public class Utils return new(); } var userHostsMap = hostsContent - .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) .Select(line => line.Trim()) // skip full-line comments .Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith('#')) // ensure line still contains valid parts .Where(line => !string.IsNullOrWhiteSpace(line) && line.Contains(' ')) - .Select(line => line.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries)) + .Select(line => line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries)) .Where(parts => parts.Length >= 2) .GroupBy(parts => parts[0]) .ToDictionary( diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index 6e4cca95..84df7538 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -301,7 +301,7 @@ public partial class CoreConfigSingboxService if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs)) { var ipItems = simpleDNSItem.DirectExpectedIPs - .Split([',', ';'], StringSplitOptions.RemoveEmptyEntries) + .Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Trim()) .Where(s => !string.IsNullOrEmpty(s)) .ToList();