From d6c81c3a9c8b7d67ed22b7f244b2ea9c79816027 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Sun, 7 Sep 2025 19:03:20 +0800 Subject: [PATCH] PreCheck --- v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 63 ++++++ v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 21 ++ v2rayN/ServiceLib/Resx/ResUI.hu.resx | 21 ++ v2rayN/ServiceLib/Resx/ResUI.resx | 21 ++ v2rayN/ServiceLib/Resx/ResUI.ru.resx | 21 ++ v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 21 ++ v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 21 ++ .../Services/ActionPrecheckService.cs | 211 ++++++++++++++++++ .../ViewModels/ProfilesViewModel.cs | 20 ++ 9 files changed, 420 insertions(+) create mode 100644 v2rayN/ServiceLib/Services/ActionPrecheckService.cs diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 6503b41c..7645f3a2 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -114,6 +114,33 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Core '{0}' does not support network type '{1}'. 的本地化字符串。 + /// + public static string CoreNotSupportNetwork { + get { + return ResourceManager.GetString("CoreNotSupportNetwork", resourceCulture); + } + } + + /// + /// 查找类似 Core '{0}' does not support protocol '{1}'. 的本地化字符串。 + /// + public static string CoreNotSupportProtocol { + get { + return ResourceManager.GetString("CoreNotSupportProtocol", resourceCulture); + } + } + + /// + /// 查找类似 Core '{0}' does not support protocol '{1}' when using transport '{2}'. 的本地化字符串。 + /// + public static string CoreNotSupportProtocolTransport { + get { + return ResourceManager.GetString("CoreNotSupportProtocolTransport", resourceCulture); + } + } + /// /// 查找类似 Note that custom configuration relies entirely on your own configuration and does not work with all settings. If you want to use the system proxy, please modify the listening port manually. 的本地化字符串。 /// @@ -2040,6 +2067,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Proxy chained node alias '{0}' does not exist. 的本地化字符串。 + /// + public static string ProxyChainedNodeTagNotExist { + get { + return ResourceManager.GetString("ProxyChainedNodeTagNotExist", resourceCulture); + } + } + + /// + /// 查找类似 Proxy chained node remark '{0}' refers to an outbound that does not support config type '{1}'. 的本地化字符串。 + /// + public static string ProxyChainedNodeTagNotSupportConfigType { + get { + return ResourceManager.GetString("ProxyChainedNodeTagNotSupportConfigType", resourceCulture); + } + } + /// /// 查找类似 Global hotkey {0} registration failed, reason: {1} 的本地化字符串。 /// @@ -2103,6 +2148,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Routing rule references outbound remark '{0}', but no outbound with this remark exists. 的本地化字符串。 + /// + public static string RoutingRuleOutboundTagNotExist { + get { + return ResourceManager.GetString("RoutingRuleOutboundTagNotExist", resourceCulture); + } + } + + /// + /// 查找类似 Routing rule outbound remark '{0}' refers to an outbound that does not support config type '{1}'. 的本地化字符串。 + /// + public static string RoutingRuleOutboundTagNotSupportConfigType { + get { + return ResourceManager.GetString("RoutingRuleOutboundTagNotSupportConfigType", resourceCulture); + } + } + /// /// 查找类似 Run as Admin 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 0c8ca393..cb1019d4 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1515,4 +1515,25 @@ Select Profile + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Routing rule references outbound remark '{0}', but no outbound with this remark exists. + + + Proxy chained node remark '{0}' refers to an outbound that does not support config type '{1}'. + + + Proxy chained node alias '{0}' does not exist. + + + Routing rule outbound remark '{0}' refers to an outbound that does not support config type '{1}'. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index 800fa00f..a3e4cc28 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1515,4 +1515,25 @@ Select Profile + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Routing rule references outbound remark '{0}', but no outbound with this remark exists. + + + Proxy chained node remark '{0}' refers to an outbound that does not support config type '{1}'. + + + Proxy chained node alias '{0}' does not exist. + + + Routing rule outbound remark '{0}' refers to an outbound that does not support config type '{1}'. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 614c8092..5c53e717 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1515,4 +1515,25 @@ Select Profile + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Routing rule references outbound remark '{0}', but no outbound with this remark exists. + + + Proxy chained node remark '{0}' refers to an outbound that does not support config type '{1}'. + + + Proxy chained node alias '{0}' does not exist. + + + Routing rule outbound remark '{0}' refers to an outbound that does not support config type '{1}'. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 294d9f34..38190d4b 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1515,4 +1515,25 @@ Select Profile + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Routing rule references outbound remark '{0}', but no outbound with this remark exists. + + + Proxy chained node remark '{0}' refers to an outbound that does not support config type '{1}'. + + + Proxy chained node alias '{0}' does not exist. + + + Routing rule outbound remark '{0}' refers to an outbound that does not support config type '{1}'. + \ 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 194a59e2..b20951e0 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1512,4 +1512,25 @@ 选择配置文件 + + 核心 '{0}' 不支持网络类型 '{1}'。 + + + 核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}'。 + + + 核心 '{0}' 不支持协议 '{1}'。 + + + 路由规则引用了出站别名 '{0}',但不存在具有该别名的出站。 + + + 代理链节点别名 '{0}' 引用的出站不支持配置类型 '{1}'。 + + + 代理链节点别名 '{0}' 不存在。 + + + 路由规则出站别名 '{0}' 引用的出站不支持配置类型 '{1}'。 + \ 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 fa84c789..859255aa 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1512,4 +1512,25 @@ Select Profile + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Routing rule references outbound remark '{0}', but no outbound with this remark exists. + + + Proxy chained node remark '{0}' refers to an outbound that does not support config type '{1}'. + + + Proxy chained node alias '{0}' does not exist. + + + Routing rule outbound remark '{0}' refers to an outbound that does not support config type '{1}'. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/ActionPrecheckService.cs b/v2rayN/ServiceLib/Services/ActionPrecheckService.cs new file mode 100644 index 00000000..f9eae5d2 --- /dev/null +++ b/v2rayN/ServiceLib/Services/ActionPrecheckService.cs @@ -0,0 +1,211 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DynamicData; +using ServiceLib.Models; + +namespace ServiceLib.Services; + +/// +/// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.). +/// Return (ok, msg) for VMs to decide. +/// +public class ActionPrecheckService +{ + private static readonly Lazy _instance = new(() => new ActionPrecheckService(AppManager.Instance.Config)); + public static ActionPrecheckService Instance => _instance.Value; + + private readonly Config _config; + + public ActionPrecheckService(Config config) + { + _config = config; + } + + private static List OneMsg(string msg) => new() { msg }; + + public async Task<(bool ok, List msgs)> CheckBeforeSetActive(string? indexId) + { + if (indexId.IsNullOrEmpty()) + { + return (false, OneMsg(ResUI.PleaseSelectServer)); + } + + var item = await AppManager.Instance.GetProfileItem(indexId); + if (item is null) + { + return (false, OneMsg(ResUI.PleaseSelectServer)); + } + + return await CheckBeforeGenerateConfig(item); + } + + public async Task<(bool ok, List msgs)> CheckBeforeGenerateConfig(ProfileItem? item) + { + if (item is null) + { + return (false, OneMsg(ResUI.PleaseSelectServer)); + } + + var msgs = new List(); + + var currentNodeMsgs = ValidateCurrentNodeAndCoreSupport(item).ToList(); + if (currentNodeMsgs.Count > 0) + { + msgs.AddRange(currentNodeMsgs); + return (false, msgs); + } + + msgs.AddRange(await ValidateRelatedNodesExistAndValid(item)); + + return (true, msgs); + } + + private IEnumerable ValidateCurrentNodeAndCoreSupport(ProfileItem item) + { + var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); + return ValidateNodeAndCoreSupport(item, coreType); + } + + /// + /// + /// Validate whether the node and chosen core combination is supported. Returns a collection of messages to show the user. + /// An empty collection means there are no blocking errors. + /// + /// + private IEnumerable ValidateNodeAndCoreSupport(ProfileItem item, ECoreType? coreType = null) + { + // sing-box does not support xhttp / kcp + // sing-box does not support transports like ws/http/httpupgrade/etc. when the node is not vmess/trojan/vless + coreType ??= AppManager.Instance.GetCoreType(item, item.ConfigType); + var net = item.GetNetwork() ?? item.Network; + + if (coreType == ECoreType.sing_box) + { + if (net is nameof(ETransport.kcp) or nameof(ETransport.xhttp)) + { + yield return string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net); + yield break; + } + + if (item.ConfigType is not (EConfigType.VMess or EConfigType.VLESS or EConfigType.Trojan)) + { + if (net is nameof(ETransport.ws) or nameof(ETransport.http) or nameof(ETransport.h2) or nameof(ETransport.quic) or nameof(ETransport.httpupgrade)) + { + yield return string.Format(ResUI.CoreNotSupportProtocolTransport, nameof(ECoreType.sing_box), item.ConfigType.ToString(), net); + yield break; + } + } + } + else if (coreType is ECoreType.Xray) + { + // Xray core does not support these protocols + if (item.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls) + { + yield return string.Format(ResUI.CoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType.ToString()); + yield break; + } + } + + yield break; // explicit for clarity; no blocking errors + } + + /// + /// Validate that nodes related to the current node (chained/routing) exist and are valid. + /// + private async Task> ValidateRelatedNodesExistAndValid(ProfileItem? item) + { + var msgs = new List(); + msgs.AddRange(await ValidateProxyChainedNodeExistAndValid(item)); + msgs.AddRange(await ValidateRoutingNodeExistAndValid(item)); + return msgs; + } + + private async Task> ValidateProxyChainedNodeExistAndValid(ProfileItem? item) + { + var msgs = new List(); + if (item is null) + { + return msgs; + } + + // prev node and next node + var subItem = await AppManager.Instance.GetSubItem(item.Subid); + if (subItem is null) + { + return msgs; + } + + var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); + var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); + + CollectProxyChainedNodeValidation(prevNode, subItem.PrevProfile, coreType, msgs); + CollectProxyChainedNodeValidation(nextNode, subItem.NextProfile, coreType, msgs); + + return msgs; + } + + private void CollectProxyChainedNodeValidation(ProfileItem? node, string tag, ECoreType coreType, List msgs) + { + if (node is not null) + { + msgs.AddRange(ValidateNodeAndCoreSupport(node, coreType)); + if (node.ConfigType is EConfigType.Custom) + { + msgs.Add(string.Format(ResUI.ProxyChainedNodeTagNotSupportConfigType, node.Remarks, node.ConfigType.ToString())); + } + } + else if (tag.IsNotEmpty()) + { + msgs.Add(string.Format(ResUI.ProxyChainedNodeTagNotExist, tag)); + } + } + + private async Task> ValidateRoutingNodeExistAndValid(ProfileItem? item) + { + var msgs = new List(); + + if (item is null) + { + return msgs; + } + + var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); + var routing = await ConfigHandler.GetDefaultRouting(_config); + if (routing == null) + { + return msgs; + } + + var rules = JsonUtils.Deserialize>(routing.RuleSet); + foreach (var ruleItem in rules ?? []) + { + if (!ruleItem.Enabled) + { + continue; + } + + var outboundTag = ruleItem.OutboundTag; + if (outboundTag.IsNullOrEmpty() || Global.OutboundTags.Contains(outboundTag)) + { + continue; + } + + var tagItem = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); + if (tagItem is null) + { + msgs.Add(string.Format(ResUI.RoutingRuleOutboundTagNotExist, outboundTag)); + continue; + } + + msgs.AddRange(ValidateNodeAndCoreSupport(tagItem, coreType)); + if (tagItem.ConfigType is EConfigType.Custom) + { + msgs.Add(string.Format(ResUI.RoutingRuleOutboundTagNotSupportConfigType, outboundTag, tagItem.ConfigType.ToString())); + } + } + + return msgs; + } +} diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index f5aeac21..cea30b45 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -594,6 +594,16 @@ public class ProfilesViewModel : MyReactiveObject return; } + var (ok, msgs) = await ActionPrecheckService.Instance.CheckBeforeSetActive(indexId); + if (msgs.Count > 0) + { + NoticeManager.Instance.Enqueue(msgs.First()); + } + if (!ok) + { + return; + } + if (await ConfigHandler.SetDefaultServerIndex(_config, indexId) == 0) { await RefreshServers(); @@ -757,6 +767,16 @@ public class ProfilesViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } + + var (ok, msgs) = await ActionPrecheckService.Instance.CheckBeforeGenerateConfig(item); + if (msgs.Count > 0) + { + NoticeManager.Instance.Enqueue(msgs.First()); + } + if (!ok) + { + return; + } if (blClipboard) { var result = await CoreConfigHandler.GenerateClientConfig(item, null);