From 11924b545f83606ba4623260ad9ceff991567906 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 12 Sep 2025 11:37:24 +0800 Subject: [PATCH] PreCheck --- v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 90 ++++++ v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 30 ++ v2rayN/ServiceLib/Resx/ResUI.hu.resx | 30 ++ v2rayN/ServiceLib/Resx/ResUI.resx | 30 ++ v2rayN/ServiceLib/Resx/ResUI.ru.resx | 30 ++ v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 30 ++ v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 30 ++ .../Services/ActionPrecheckService.cs | 282 ++++++++++++++++++ .../ViewModels/ProfilesViewModel.cs | 20 ++ 9 files changed, 572 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 483fb417..89eab190 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. 的本地化字符串。 /// @@ -267,6 +294,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Group '{0}' is empty. Please add at least one node. 的本地化字符串。 + /// + public static string GroupEmpty { + get { + return ResourceManager.GetString("GroupEmpty", resourceCulture); + } + } + + /// + /// 查找类似 The group "{0}" cannot reference itself. 的本地化字符串。 + /// + public static string GroupSelfReference { + get { + return ResourceManager.GetString("GroupSelfReference", resourceCulture); + } + } + /// /// 查找类似 This is not the correct configuration, please check 的本地化字符串。 /// @@ -294,6 +339,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 The {0} property is invalid, please check. 的本地化字符串。 + /// + public static string InvalidProperty { + get { + return ResourceManager.GetString("InvalidProperty", resourceCulture); + } + } + /// /// 查找类似 Invalid address (URL) 的本地化字符串。 /// @@ -2004,6 +2058,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Node alias '{0}' does not exist. 的本地化字符串。 + /// + public static string NodeTagNotExist { + get { + return ResourceManager.GetString("NodeTagNotExist", resourceCulture); + } + } + /// /// 查找类似 Non-VMess or SS protocol 的本地化字符串。 /// @@ -2112,6 +2175,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Policy group: 的本地化字符串。 + /// + public static string PolicyGroupPrefix { + get { + return ResourceManager.GetString("PolicyGroupPrefix", resourceCulture); + } + } + + /// + /// 查找类似 Proxy chained: 的本地化字符串。 + /// + public static string ProxyChainedPrefix { + get { + return ResourceManager.GetString("ProxyChainedPrefix", resourceCulture); + } + } + /// /// 查找类似 Global hotkey {0} registration failed, reason: {1} 的本地化字符串。 /// @@ -2175,6 +2256,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Routing rule outbound: 的本地化字符串。 + /// + public static string RoutingRuleOutboundPrefix { + get { + return ResourceManager.GetString("RoutingRuleOutboundPrefix", resourceCulture); + } + } + /// /// 查找类似 Run as Admin 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 9893d47d..dd25cc53 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1563,4 +1563,34 @@ Multi-Configuration Fallback by Xray + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + The group "{0}" cannot reference itself. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index aa1a3b13..1a5e4181 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1563,4 +1563,34 @@ Multi-Configuration Fallback by Xray + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + The group "{0}" cannot reference itself. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 85875a90..17be5e6d 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1563,4 +1563,34 @@ Multi-Configuration Fallback by Xray + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + The group "{0}" cannot reference itself. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 1dfa268e..71227c72 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1563,4 +1563,34 @@ Multi-Configuration Fallback by Xray + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + The group "{0}" cannot reference itself. + \ 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 516c2f4a..91d959e8 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1560,4 +1560,34 @@ 多配置文件故障转移 Xray + + 核心 '{0}' 不支持网络类型 '{1}'。 + + + 核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}'。 + + + 核心 '{0}' 不支持协议 '{1}'。 + + + 代理链: + + + 路由规则出站: + + + 策略组: + + + 节点别名 '{0}' 不存在。 + + + 组“{0}”为空。请至少添加一个节点。 + + + {0}属性无效,请检查 + + + {0} 分组不能引用自身 + \ 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 c041de1f..2d166484 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1560,4 +1560,34 @@ Multi-Configuration Fallback by Xray + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + The group "{0}" cannot reference itself. + \ 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..9aa6d8c6 --- /dev/null +++ b/v2rayN/ServiceLib/Services/ActionPrecheckService.cs @@ -0,0 +1,282 @@ +namespace ServiceLib.Services; + +/// +/// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.). +/// +public class ActionPrecheckService(Config config) +{ + private static readonly Lazy _instance = new(() => new ActionPrecheckService(AppManager.Instance.Config)); + public static ActionPrecheckService Instance => _instance.Value; + + private readonly Config _config = config; + + public async Task> CheckBeforeSetActive(string? indexId) + { + if (indexId.IsNullOrEmpty()) + { + return [ResUI.PleaseSelectServer]; + } + + var item = await AppManager.Instance.GetProfileItem(indexId); + if (item is null) + { + return [ResUI.PleaseSelectServer]; + } + + return await CheckBeforeGenerateConfig(item); + } + + public async Task> CheckBeforeGenerateConfig(ProfileItem? item) + { + if (item is null) + { + return [ResUI.PleaseSelectServer]; + } + + var errors = new List(); + + errors.AddRange(await ValidateCurrentNodeAndCoreSupport(item)); + errors.AddRange(await ValidateRelatedNodesExistAndValid(item)); + + return errors; + } + + private async Task> ValidateCurrentNodeAndCoreSupport(ProfileItem item) + { + if (item.ConfigType == EConfigType.Custom) + { + return []; + } + var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); + return await ValidateNodeAndCoreSupport(item, coreType); + } + + private async Task> ValidateNodeAndCoreSupport(ProfileItem item, ECoreType? coreType = null) + { + var errors = new List(); + + // 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); + + if (item.ConfigType is EConfigType.Custom) + { + errors.Add(string.Format(ResUI.CoreNotSupportProtocol, coreType.ToString(), item.ConfigType.ToString())); + return errors; + } + + if (!item.IsComplex()) + { + if (item.Address.IsNullOrEmpty()) + { + errors.Add(string.Format(ResUI.InvalidProperty, "Address")); + return errors; + } + + if (item.Port is <= 0 or >= 65536) + { + errors.Add(string.Format(ResUI.InvalidProperty, "Port")); + return errors; + } + + + switch (item.ConfigType) + { + case EConfigType.VMess: + if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) + errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + break; + + case EConfigType.VLESS: + if (item.Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(item.Id) && item.Id.Length > 30)) + errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + if (!Global.Flows.Contains(item.Flow)) + errors.Add(string.Format(ResUI.InvalidProperty, "Flow")); + break; + + case EConfigType.Shadowsocks: + if (item.Id.IsNullOrEmpty()) + errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + if (string.IsNullOrEmpty(item.Security) || !Global.SsSecuritiesInSingbox.Contains(item.Security)) + errors.Add(string.Format(ResUI.InvalidProperty, "Security")); + break; + } + + if ((item.ConfigType is EConfigType.VLESS or EConfigType.Trojan) + && item.StreamSecurity == Global.StreamSecurityReality + && item.PublicKey.IsNullOrEmpty()) + { + errors.Add(string.Format(ResUI.InvalidProperty, "PublicKey")); + } + + if (errors.Count > 0) + { + return errors; + } + } + + if (item.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + ProfileGroupItemManager.Instance.TryGet(item.IndexId, out var group); + if (group is null || group.ChildItems.IsNullOrEmpty()) + { + errors.Add(string.Format(ResUI.GroupEmpty, item.Remarks)); + return errors; + } + + foreach (var child in Utils.String2List(group.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)); + continue; + } + + // self-reference check + if (childItem.IndexId == item.IndexId) + { + childErrors.Add(string.Format(ResUI.GroupSelfReference, childItem.Remarks)); + continue; + } + + if (childItem.ConfigType is EConfigType.Custom or EConfigType.ProxyChain) + { + childErrors.Add(string.Format(ResUI.InvalidProperty, childItem.Remarks)); + continue; + } + + childErrors.AddRange(await ValidateNodeAndCoreSupport(childItem, coreType)); + errors.AddRange(childErrors); + } + return errors; + } + + var net = item.GetNetwork() ?? item.Network; + + if (coreType == ECoreType.sing_box) + { + if (net is nameof(ETransport.kcp) or nameof(ETransport.xhttp)) + { + errors.Add(string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net)); + return errors; + } + + if (item.ConfigType is not (EConfigType.VMess or EConfigType.VLESS or EConfigType.Trojan)) + { + if (net is nameof(ETransport.ws) or nameof(ETransport.http) or nameof(ETransport.h2) or nameof(ETransport.quic) or nameof(ETransport.httpupgrade)) + { + errors.Add(string.Format(ResUI.CoreNotSupportProtocolTransport, nameof(ECoreType.sing_box), item.ConfigType.ToString(), net)); + return errors; + } + } + } + else if (coreType is ECoreType.Xray) + { + // Xray core does not support these protocols + if (item.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls) + { + errors.Add(string.Format(ResUI.CoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType.ToString())); + return errors; + } + } + + return errors.Select(s => $"{item.Remarks}: {s}").ToList(); + } + + private async Task> ValidateRelatedNodesExistAndValid(ProfileItem? item) + { + var errors = new List(); + errors.AddRange(await ValidateProxyChainedNodeExistAndValid(item)); + errors.AddRange(await ValidateRoutingNodeExistAndValid(item)); + return errors; + } + + private async Task> ValidateProxyChainedNodeExistAndValid(ProfileItem? item) + { + var errors = new List(); + if (item is null) + { + return errors; + } + + // prev node and next node + var subItem = await AppManager.Instance.GetSubItem(item.Subid); + if (subItem is null) + { + return errors; + } + + var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); + var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); + + await CollectProxyChainedNodeValidation(prevNode, subItem.PrevProfile, coreType, errors); + await CollectProxyChainedNodeValidation(nextNode, subItem.NextProfile, coreType, errors); + + return errors; + } + + private async Task CollectProxyChainedNodeValidation(ProfileItem? node, string tag, ECoreType coreType, List errors) + { + if (node is not null) + { + var nodeErrors = await ValidateNodeAndCoreSupport(node, coreType); + errors.AddRange(nodeErrors.Select(s => ResUI.ProxyChainedPrefix + s)); + } + else if (tag.IsNotEmpty()) + { + errors.Add(ResUI.ProxyChainedPrefix + string.Format(ResUI.NodeTagNotExist, tag)); + } + } + + private async Task> ValidateRoutingNodeExistAndValid(ProfileItem? item) + { + var errors = new List(); + + if (item is null) + { + return errors; + } + + var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); + var routing = await ConfigHandler.GetDefaultRouting(_config); + if (routing == null) + { + return errors; + } + + var rules = JsonUtils.Deserialize>(routing.RuleSet); + foreach (var ruleItem in rules ?? []) + { + if (!ruleItem.Enabled) + { + continue; + } + + var outboundTag = ruleItem.OutboundTag; + if (outboundTag.IsNullOrEmpty() || Global.OutboundTags.Contains(outboundTag)) + { + continue; + } + + var tagItem = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); + if (tagItem is null) + { + errors.Add(ResUI.RoutingRuleOutboundPrefix + string.Format(ResUI.NodeTagNotExist, outboundTag)); + continue; + } + + var tagErrors = await ValidateNodeAndCoreSupport(tagItem, coreType); + errors.AddRange(tagErrors.Select(s => ResUI.RoutingRuleOutboundPrefix + s)); + } + + return errors; + } +} diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 8aff5950..c3920863 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -596,6 +596,16 @@ public class ProfilesViewModel : MyReactiveObject return; } + var msgs = await ActionPrecheckService.Instance.CheckBeforeSetActive(indexId); + foreach (var msg in msgs) + { + NoticeManager.Instance.SendMessage(msg); + } + if (msgs.Count > 0) + { + NoticeManager.Instance.Enqueue(msgs.First()); + } + if (await ConfigHandler.SetDefaultServerIndex(_config, indexId) == 0) { await RefreshServers(); @@ -759,6 +769,16 @@ public class ProfilesViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } + + var msgs = await ActionPrecheckService.Instance.CheckBeforeGenerateConfig(item); + foreach (var msg in msgs) + { + NoticeManager.Instance.SendMessage(msg); + } + if (msgs.Count > 0) + { + NoticeManager.Instance.Enqueue(msgs.First()); + } if (blClipboard) { var result = await CoreConfigHandler.GenerateClientConfig(item, null);