diff --git a/v2rayN/ServiceLib/GlobalUsings.cs b/v2rayN/ServiceLib/GlobalUsings.cs index d38ccc06..4553cbc4 100644 --- a/v2rayN/ServiceLib/GlobalUsings.cs +++ b/v2rayN/ServiceLib/GlobalUsings.cs @@ -24,6 +24,7 @@ global using ServiceLib.Common; global using ServiceLib.Enums; global using ServiceLib.Events; global using ServiceLib.Handler; +global using ServiceLib.Handler.Builder; global using ServiceLib.Handler.Fmt; global using ServiceLib.Handler.SysProxy; global using ServiceLib.Helper; diff --git a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs new file mode 100644 index 00000000..23a495f8 --- /dev/null +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -0,0 +1,331 @@ +namespace ServiceLib.Handler.Builder; + +public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult) +{ + public bool Success => ValidatorResult.Success; +} + +public class CoreConfigContextBuilder +{ + /// + /// Builds a for the given node, resolves its proxy map, + /// and processes outbound nodes referenced by routing rules. + /// + public static async Task Build(Config config, ProfileItem node) + { + var runCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType); + var coreType = runCoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray; + var context = new CoreConfigContext() + { + Node = node, + RunCoreType = runCoreType, + AllProxiesMap = [], + AppConfig = config, + FullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(coreType), + IsTunEnabled = config.TunModeItem.EnableTun, + SimpleDnsItem = config.SimpleDNSItem, + ProtectDomainList = [], + TunProtectSsPort = 0, + ProxyRelaySsPort = 0, + RawDnsItem = await AppManager.Instance.GetDNSItem(coreType), + RoutingItem = await ConfigHandler.GetDefaultRouting(config), + }; + var validatorResult = NodeValidatorResult.Empty(); + var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node); + if (!nodeValidatorResult.Success) + { + return new CoreConfigContextBuilderResult(context, nodeValidatorResult); + } + context = context with { Node = actNode }; + validatorResult.Warnings.AddRange(nodeValidatorResult.Warnings); + if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true)) + { + var rules = JsonUtils.Deserialize>(context.RoutingItem?.RuleSet) ?? []; + foreach (var ruleItem in rules.Where(ruleItem => ruleItem.Enabled && !Global.OutboundTags.Contains(ruleItem.OutboundTag))) + { + if (ruleItem.OutboundTag.IsNullOrEmpty()) + { + validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleEmptyOutboundTag, ruleItem.Remarks)); + ruleItem.OutboundTag = Global.ProxyTag; + continue; + } + var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag); + if (ruleOutboundNode == null) + { + validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleOutboundNodeNotFound, ruleItem.Remarks, ruleItem.OutboundTag)); + ruleItem.OutboundTag = Global.ProxyTag; + continue; + } + + var (actRuleNode, ruleNodeValidatorResult) = await ResolveNodeAsync(context, ruleOutboundNode, false); + validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Warnings.Select(w => + string.Format(ResUI.MsgRoutingRuleOutboundNodeWarning, ruleItem.Remarks, ruleItem.OutboundTag, w))); + if (!ruleNodeValidatorResult.Success) + { + validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e => + string.Format(ResUI.MsgRoutingRuleOutboundNodeError, ruleItem.Remarks, ruleItem.OutboundTag, e))); + ruleItem.OutboundTag = Global.ProxyTag; + continue; + } + + context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode; + } + } + + return new CoreConfigContextBuilderResult(context, validatorResult); + } + + /// + /// Resolves a node into the context, optionally wrapping it in a subscription-level proxy chain. + /// Returns the effective (possibly replaced) node and the validation result. + /// + public static async Task<(ProfileItem, NodeValidatorResult)> ResolveNodeAsync(CoreConfigContext context, + ProfileItem node, + bool includeSubChain = true) + { + if (node.IndexId.IsNullOrEmpty()) + { + return (node, NodeValidatorResult.Empty()); + } + + if (includeSubChain) + { + var (virtualChainNode, chainValidatorResult) = await BuildSubscriptionChainNodeAsync(node); + if (virtualChainNode != null) + { + context.AllProxiesMap[virtualChainNode.IndexId] = virtualChainNode; + var (resolvedNode, resolvedResult) = await ResolveNodeAsync(context, virtualChainNode, false); + resolvedResult.Warnings.InsertRange(0, chainValidatorResult.Warnings); + return (resolvedNode, resolvedResult); + } + // Chain not built but warnings may still exist (e.g. missing profiles) + if (chainValidatorResult.Warnings.Count > 0) + { + var fillResult = await RegisterNodeAsync(context, node); + fillResult.Warnings.InsertRange(0, chainValidatorResult.Warnings); + return (node, fillResult); + } + } + + var registerResult = await RegisterNodeAsync(context, node); + return (node, registerResult); + } + + /// + /// If the node's subscription defines prev/next profiles, creates a virtual + /// node that wraps them together. + /// Returns null as the chain item when no chain is needed. + /// Any warnings (e.g. missing prev/next profile) are returned in the validator result. + /// + private static async Task<(ProfileItem? ChainNode, NodeValidatorResult ValidatorResult)> BuildSubscriptionChainNodeAsync(ProfileItem node) + { + var result = NodeValidatorResult.Empty(); + + if (node.Subid.IsNullOrEmpty()) + { + return (null, result); + } + + var subItem = await AppManager.Instance.GetSubItem(node.Subid); + if (subItem == null) + { + return (null, result); + } + + ProfileItem? prevNode = null; + ProfileItem? nextNode = null; + + if (!subItem.PrevProfile.IsNullOrEmpty()) + { + prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + if (prevNode == null) + { + result.Warnings.Add(string.Format(ResUI.MsgSubscriptionPrevProfileNotFound, subItem.PrevProfile)); + } + } + if (!subItem.NextProfile.IsNullOrEmpty()) + { + nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); + if (nextNode == null) + { + result.Warnings.Add(string.Format(ResUI.MsgSubscriptionNextProfileNotFound, subItem.NextProfile)); + } + } + + if (prevNode is null && nextNode is null) + { + return (null, result); + } + + // Build new proxy chain node + var chainNode = new ProfileItem() + { + IndexId = $"inner-{Utils.GetGuid(false)}", + ConfigType = EConfigType.ProxyChain, + CoreType = node.CoreType ?? ECoreType.Xray, + }; + List childItems = [prevNode?.IndexId, node.IndexId, nextNode?.IndexId]; + var chainExtraItem = chainNode.GetProtocolExtra() with + { + GroupType = chainNode.ConfigType.ToString(), + ChildItems = string.Join(",", childItems.Where(x => !x.IsNullOrEmpty())), + }; + chainNode.SetProtocolExtra(chainExtraItem); + return (chainNode, result); + } + + /// + /// Dispatches registration to either or + /// based on the node's config type. + /// + private static async Task RegisterNodeAsync(CoreConfigContext context, ProfileItem node) + { + if (node.ConfigType.IsGroupType()) + { + return await RegisterGroupNodeAsync(context, node); + } + else + { + return RegisterSingleNodeAsync(context, node); + } + } + + /// + /// Validates a single (non-group) node and, on success, adds it to the proxy map + /// and records any domain addresses that should bypass the proxy. + /// + private static NodeValidatorResult RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node) + { + if (node.ConfigType.IsGroupType()) + { + return NodeValidatorResult.Empty(); + } + + var nodeValidatorResult = NodeValidator.Validate(node, context.RunCoreType); + if (!nodeValidatorResult.Success) + { + return nodeValidatorResult; + } + + context.AllProxiesMap[node.IndexId] = node; + + var address = node.Address; + if (Utils.IsDomain(address)) + { + context.ProtectDomainList.Add(address); + } + + if (!node.EchConfigList.IsNullOrEmpty()) + { + var echQuerySni = node.Sni; + if (node.StreamSecurity == Global.StreamSecurity + && node.EchConfigList?.Contains("://") == true) + { + var idx = node.EchConfigList.IndexOf('+'); + echQuerySni = idx > 0 ? node.EchConfigList[..idx] : node.Sni; + } + + if (Utils.IsDomain(echQuerySni)) + { + context.ProtectDomainList.Add(echQuerySni); + } + } + + return nodeValidatorResult; + } + + /// + /// Entry point for registering a group node. Initialises the visited/ancestor sets + /// and delegates to . + /// + private static async Task RegisterGroupNodeAsync(CoreConfigContext context, + ProfileItem node) + { + if (!node.ConfigType.IsGroupType()) + { + return NodeValidatorResult.Empty(); + } + + HashSet ancestors = [node.IndexId]; + HashSet globalVisited = [node.IndexId]; + return await TraverseGroupNodeAsync(context, node, globalVisited, ancestors); + } + + /// + /// Recursively walks the children of a group node, registering valid leaf nodes + /// and nested groups. Detects cycles via and + /// deduplicates shared nodes via . + /// + private static async Task TraverseGroupNodeAsync( + CoreConfigContext context, + ProfileItem node, + HashSet globalVisitedGroup, + HashSet ancestorsGroup) + { + var (groupChildList, _) = await GroupProfileManager.GetChildProfileItems(node); + List childIndexIdList = []; + var childNodeValidatorResult = NodeValidatorResult.Empty(); + foreach (var childNode in groupChildList) + { + if (ancestorsGroup.Contains(childNode.IndexId)) + { + childNodeValidatorResult.Errors.Add( + string.Format(ResUI.MsgGroupCycleDependency, node.Remarks, childNode.Remarks)); + continue; + } + + if (globalVisitedGroup.Contains(childNode.IndexId)) + { + childIndexIdList.Add(childNode.IndexId); + continue; + } + + if (!childNode.ConfigType.IsGroupType()) + { + var childNodeResult = RegisterSingleNodeAsync(context, childNode); + childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w => + string.Format(ResUI.MsgGroupChildNodeWarning, node.Remarks, childNode.Remarks, w))); + childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e => + string.Format(ResUI.MsgGroupChildNodeError, node.Remarks, childNode.Remarks, e))); + if (!childNodeResult.Success) + { + continue; + } + + globalVisitedGroup.Add(childNode.IndexId); + childIndexIdList.Add(childNode.IndexId); + continue; + } + + var newAncestorsGroup = new HashSet(ancestorsGroup) { childNode.IndexId }; + var childGroupResult = + await TraverseGroupNodeAsync(context, childNode, globalVisitedGroup, newAncestorsGroup); + childNodeValidatorResult.Warnings.AddRange(childGroupResult.Warnings.Select(w => + string.Format(ResUI.MsgGroupChildGroupNodeWarning, node.Remarks, childNode.Remarks, w))); + childNodeValidatorResult.Errors.AddRange(childGroupResult.Errors.Select(e => + string.Format(ResUI.MsgGroupChildGroupNodeError, node.Remarks, childNode.Remarks, e))); + if (!childGroupResult.Success) + { + continue; + } + + globalVisitedGroup.Add(childNode.IndexId); + childIndexIdList.Add(childNode.IndexId); + } + + if (childIndexIdList.Count == 0) + { + childNodeValidatorResult.Errors.Add(string.Format(ResUI.MsgGroupNoValidChildNode, node.Remarks)); + return childNodeValidatorResult; + } + else + { + childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors); + childNodeValidatorResult.Errors.Clear(); + } + + node.SetProtocolExtra(node.GetProtocolExtra() with { ChildItems = Utils.List2String(childIndexIdList), }); + context.AllProxiesMap[node.IndexId] = node; + return childNodeValidatorResult; + } +} diff --git a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs new file mode 100644 index 00000000..a6adf1c2 --- /dev/null +++ b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs @@ -0,0 +1,175 @@ +namespace ServiceLib.Handler.Builder; + +public record NodeValidatorResult(List Errors, List Warnings) +{ + public bool Success => Errors.Count == 0; + + public static NodeValidatorResult Empty() + { + return new NodeValidatorResult([], []); + } +} + +public class NodeValidator +{ + // Static validator rules + private static readonly HashSet SingboxUnsupportedTransports = + [nameof(ETransport.kcp), nameof(ETransport.xhttp)]; + + private static readonly HashSet SingboxTransportSupportedProtocols = + [EConfigType.VMess, EConfigType.VLESS, EConfigType.Trojan, EConfigType.Shadowsocks]; + + private static readonly HashSet SingboxShadowsocksAllowedTransports = + [nameof(ETransport.tcp), nameof(ETransport.ws), nameof(ETransport.quic)]; + + public static NodeValidatorResult Validate(ProfileItem item, ECoreType coreType) + { + var v = new ValidationContext(); + ValidateNodeAndCoreSupport(item, coreType, v); + return v.ToResult(); + } + + private class ValidationContext + { + public List Errors { get; } = []; + public List Warnings { get; } = []; + + public void Error(string message) + { + Errors.Add(message); + } + + public void Warning(string message) + { + Warnings.Add(message); + } + + public void Assert(bool condition, string errorMsg) + { + if (!condition) + { + Error(errorMsg); + } + } + + public NodeValidatorResult ToResult() + { + return new NodeValidatorResult(Errors, Warnings); + } + } + + private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreType, ValidationContext v) + { + if (item.ConfigType is EConfigType.Custom) + { + return; + } + + if (item.ConfigType.IsGroupType()) + { + // Group logic is handled in ValidateGroupNode + return; + } + + // Basic Property Validation + v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Address")); + v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, "Port")); + + // Network & Core Logic + var net = item.GetNetwork(); + if (coreType == ECoreType.sing_box) + { + var transportError = ValidateSingboxTransport(item.ConfigType, net); + if (transportError != null) + v.Error(transportError); + + if (!Global.SingboxSupportConfigType.Contains(item.ConfigType)) + { + v.Error(string.Format(ResUI.MsgCoreNotSupportProtocol, nameof(ECoreType.sing_box), item.ConfigType)); + } + } + else if (coreType is ECoreType.Xray) + { + if (!Global.XraySupportConfigType.Contains(item.ConfigType)) + { + v.Error(string.Format(ResUI.MsgCoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType)); + } + } + + // Protocol Specifics + var protocolExtra = item.GetProtocolExtra(); + switch (item.ConfigType) + { + case EConfigType.VMess: + v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password), + string.Format(ResUI.MsgInvalidProperty, "Password")); + break; + + case EConfigType.VLESS: + v.Assert( + !item.Password.IsNullOrEmpty() + && (Utils.IsGuidByParse(item.Password) || item.Password.Length <= 30), + string.Format(ResUI.MsgInvalidProperty, "Password") + ); + v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty), + string.Format(ResUI.MsgInvalidProperty, "Flow")); + break; + + case EConfigType.Shadowsocks: + v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Password")); + v.Assert( + !string.IsNullOrEmpty(protocolExtra.SsMethod) && + Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod), + string.Format(ResUI.MsgInvalidProperty, "SsMethod")); + break; + } + + // TLS & Security + if (item.StreamSecurity == Global.StreamSecurity) + { + if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0 && + !item.CertSha.IsNullOrEmpty()) + { + v.Error(string.Format(ResUI.MsgInvalidProperty, "TLS Certificate")); + } + } + + if (item.StreamSecurity == Global.StreamSecurityReality) + { + v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "PublicKey")); + } + + if (item.Network == nameof(ETransport.xhttp) && !item.Extra.IsNullOrEmpty()) + { + if (JsonUtils.ParseJson(item.Extra) is null) + { + v.Error(string.Format(ResUI.MsgInvalidProperty, "XHTTP Extra")); + } + } + } + + private static string? ValidateSingboxTransport(EConfigType configType, string net) + { + // sing-box does not support xhttp / kcp transports + if (SingboxUnsupportedTransports.Contains(net)) + { + return string.Format(ResUI.MsgCoreNotSupportNetwork, nameof(ECoreType.sing_box), net); + } + + // sing-box does not support non-tcp transports for protocols other than vmess/trojan/vless/shadowsocks + if (!SingboxTransportSupportedProtocols.Contains(configType) && net != nameof(ETransport.tcp)) + { + return string.Format(ResUI.MsgCoreNotSupportProtocolTransport, + nameof(ECoreType.sing_box), configType.ToString(), net); + } + + // sing-box shadowsocks only supports tcp/ws/quic transports + if (configType == EConfigType.Shadowsocks && !SingboxShadowsocksAllowedTransports.Contains(net)) + { + return string.Format(ResUI.MsgCoreNotSupportProtocolTransport, + nameof(ECoreType.sing_box), configType.ToString(), net); + } + + return null; + } +} diff --git a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs index a38faafa..1806ad26 100644 --- a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs @@ -93,13 +93,18 @@ public static class CoreConfigHandler public static async Task GenerateClientSpeedtestConfig(Config config, string fileName, List selecteds, ECoreType coreType) { var result = new RetResult(); - var context = await BuildCoreConfigContext(config, new()); + var dummyNode = new ProfileItem + { + CoreType = coreType + }; + var builderResult = await CoreConfigContextBuilder.Build(config, dummyNode); + var context = builderResult.Context; var ids = selecteds.Where(serverTestItem => !serverTestItem.IndexId.IsNullOrEmpty()) .Select(serverTestItem => serverTestItem.IndexId); var nodes = await AppManager.Instance.GetProfileItemsByIndexIds(ids); foreach (var node in nodes) { - var actNode = await FillNodeContext(context, node, true); + var (actNode, _) = await CoreConfigContextBuilder.ResolveNodeAsync(context, node, true); if (node.IndexId == actNode.IndexId) { continue; @@ -146,128 +151,4 @@ public static class CoreConfigHandler await File.WriteAllTextAsync(fileName, result.Data.ToString()); return result; } - - public static async Task BuildCoreConfigContext(Config config, ProfileItem node) - { - var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray; - var context = new CoreConfigContext() - { - Node = node, - AllProxiesMap = [], - AppConfig = config, - FullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(coreType), - IsTunEnabled = config.TunModeItem.EnableTun, - SimpleDnsItem = config.SimpleDNSItem, - ProtectDomainList = [], - TunProtectSsPort = 0, - ProxyRelaySsPort = 0, - RawDnsItem = await AppManager.Instance.GetDNSItem(coreType), - RoutingItem = await ConfigHandler.GetDefaultRouting(config), - }; - context = context with - { - Node = await FillNodeContext(context, node) - }; - if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true)) - { - var rules = JsonUtils.Deserialize>(context.RoutingItem?.RuleSet); - foreach (var ruleItem in rules.Where(ruleItem => !Global.OutboundTags.Contains(ruleItem.OutboundTag))) - { - var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag); - if (ruleOutboundNode != null) - { - var ruleOutboundNodeAct = await FillNodeContext(context, ruleOutboundNode, false); - context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = ruleOutboundNodeAct; - } - } - } - return context; - } - - private static async Task FillNodeContext(CoreConfigContext context, ProfileItem node, bool includeSubChain = true) - { - if (node.IndexId.IsNullOrEmpty()) - { - return node; - } - var newItems = new List { node }; - if (node.ConfigType.IsGroupType()) - { - var (groupChildList, _) = await GroupProfileManager.GetChildProfileItems(node); - foreach (var childItem in groupChildList.Where(childItem => !context.AllProxiesMap.ContainsKey(childItem.IndexId))) - { - await FillNodeContext(context, childItem, false); - } - node.SetProtocolExtra(node.GetProtocolExtra() with - { - ChildItems = Utils.List2String(groupChildList.Select(n => n.IndexId).ToList()), - }); - newItems.AddRange(groupChildList); - } - context.AllProxiesMap[node.IndexId] = node; - - foreach (var item in newItems) - { - var address = item.Address; - if (Utils.IsDomain(address)) - { - context.ProtectDomainList.Add(address); - } - - if (item.EchConfigList.IsNullOrEmpty()) - { - continue; - } - - var echQuerySni = item.Sni; - if (item.StreamSecurity == Global.StreamSecurity - && item.EchConfigList?.Contains("://") == true) - { - var idx = item.EchConfigList.IndexOf('+'); - echQuerySni = idx > 0 ? item.EchConfigList[..idx] : item.Sni; - } - if (!Utils.IsDomain(echQuerySni)) - { - continue; - } - context.ProtectDomainList.Add(echQuerySni); - } - - if (!includeSubChain || node.Subid.IsNullOrEmpty()) - { - return node; - } - - var subItem = await AppManager.Instance.GetSubItem(node.Subid); - if (subItem == null) - { - return node; - } - var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); - var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); - if (prevNode is null && nextNode is null) - { - return node; - } - - var prevNodeAct = prevNode is null ? null : await FillNodeContext(context, prevNode, false); - var nextNodeAct = nextNode is null ? null : await FillNodeContext(context, nextNode, false); - - // Build new proxy chain node - var chainNode = new ProfileItem() - { - IndexId = $"inner-{Utils.GetGuid(false)}", - ConfigType = EConfigType.ProxyChain, - CoreType = node.CoreType ?? ECoreType.Xray, - }; - List childItems = [prevNodeAct?.IndexId, node.IndexId, nextNodeAct?.IndexId]; - var chainExtraItem = chainNode.GetProtocolExtra() with - { - GroupType = chainNode.ConfigType.ToString(), - ChildItems = string.Join(",", childItems.Where(x => !x.IsNullOrEmpty())), - }; - chainNode.SetProtocolExtra(chainExtraItem); - context.AllProxiesMap[chainNode.IndexId] = chainNode; - return chainNode; - } } diff --git a/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs b/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs deleted file mode 100644 index ee3cd5a0..00000000 --- a/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs +++ /dev/null @@ -1,352 +0,0 @@ -namespace ServiceLib.Manager; - -/// -/// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.). -/// -public class ActionPrecheckManager -{ - private static readonly Lazy _instance = new(); - public static ActionPrecheckManager Instance => _instance.Value; - - // sing-box supported transports for different protocol types - private static readonly HashSet SingboxUnsupportedTransports = [nameof(ETransport.kcp), nameof(ETransport.xhttp)]; - - private static readonly HashSet SingboxTransportSupportedProtocols = - [EConfigType.VMess, EConfigType.VLESS, EConfigType.Trojan, EConfigType.Shadowsocks]; - - private static readonly HashSet SingboxShadowsocksAllowedTransports = - [nameof(ETransport.tcp), nameof(ETransport.ws), nameof(ETransport.quic)]; - - public async Task> Check(string? indexId) - { - if (indexId.IsNullOrEmpty()) - { - return [ResUI.PleaseSelectServer]; - } - - var item = await AppManager.Instance.GetProfileItem(indexId); - if (item is null) - { - return [ResUI.PleaseSelectServer]; - } - - return await Check(item); - } - - public async Task> Check(ProfileItem? item) - { - if (item is null) - { - return [ResUI.PleaseSelectServer]; - } - - var errors = new List(); - - errors.AddRange(await ValidateCurrentNodeAndCoreSupport(item)); - errors.AddRange(await ValidateRelatedNodesExistAndValid(item)); - - return errors; - } - - private async Task> ValidateCurrentNodeAndCoreSupport(ProfileItem item) - { - if (item.ConfigType == EConfigType.Custom) - { - return []; - } - - var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); - return await ValidateNodeAndCoreSupport(item, coreType); - } - - private async Task> ValidateNodeAndCoreSupport(ProfileItem item, ECoreType? coreType = null) - { - var errors = new List(); - - coreType ??= AppManager.Instance.GetCoreType(item, item.ConfigType); - - if (item.ConfigType is EConfigType.Custom) - { - errors.Add(string.Format(ResUI.NotSupportProtocol, item.ConfigType.ToString())); - return errors; - } - else if (item.ConfigType.IsGroupType()) - { - var groupErrors = await ValidateGroupNode(item, coreType); - errors.AddRange(groupErrors); - return errors; - } - else if (!item.IsComplex()) - { - var normalErrors = await ValidateNormalNode(item, coreType); - errors.AddRange(normalErrors); - return errors; - } - - return errors; - } - - private async Task> ValidateNormalNode(ProfileItem item, ECoreType? coreType = null) - { - var errors = new List(); - - if (item.Address.IsNullOrEmpty()) - { - errors.Add(string.Format(ResUI.InvalidProperty, "Address")); - return errors; - } - - if (item.Port is <= 0 or > 65535) - { - errors.Add(string.Format(ResUI.InvalidProperty, "Port")); - return errors; - } - - var net = item.GetNetwork(); - - if (coreType == ECoreType.sing_box) - { - var transportError = ValidateSingboxTransport(item.ConfigType, net); - if (transportError != null) - { - errors.Add(transportError); - } - - if (!Global.SingboxSupportConfigType.Contains(item.ConfigType)) - { - errors.Add(string.Format(ResUI.CoreNotSupportProtocol, - nameof(ECoreType.sing_box), item.ConfigType.ToString())); - } - } - else if (coreType is ECoreType.Xray) - { - // Xray core does not support these protocols - if (!Global.XraySupportConfigType.Contains(item.ConfigType)) - { - errors.Add(string.Format(ResUI.CoreNotSupportProtocol, - nameof(ECoreType.Xray), item.ConfigType.ToString())); - } - } - - var protocolExtra = item.GetProtocolExtra(); - - switch (item.ConfigType) - { - case EConfigType.VMess: - if (item.Password.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Password)) - { - errors.Add(string.Format(ResUI.InvalidProperty, "Password")); - } - - break; - - case EConfigType.VLESS: - if (item.Password.IsNullOrEmpty() || (!Utils.IsGuidByParse(item.Password) && item.Password.Length > 30)) - { - errors.Add(string.Format(ResUI.InvalidProperty, "Password")); - } - - if (!Global.Flows.Contains(protocolExtra.Flow ?? string.Empty)) - { - errors.Add(string.Format(ResUI.InvalidProperty, "Flow")); - } - - break; - - case EConfigType.Shadowsocks: - if (item.Password.IsNullOrEmpty()) - { - errors.Add(string.Format(ResUI.InvalidProperty, "Password")); - } - - if (string.IsNullOrEmpty(protocolExtra.SsMethod) || !Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod)) - { - errors.Add(string.Format(ResUI.InvalidProperty, "SsMethod")); - } - - break; - } - - if (item.StreamSecurity == Global.StreamSecurity) - { - // check certificate validity - if (!item.Cert.IsNullOrEmpty() - && (CertPemManager.ParsePemChain(item.Cert).Count == 0) - && !item.CertSha.IsNullOrEmpty()) - { - errors.Add(string.Format(ResUI.InvalidProperty, "TLS Certificate")); - } - } - - if (item.StreamSecurity == Global.StreamSecurityReality) - { - if (item.PublicKey.IsNullOrEmpty()) - { - errors.Add(string.Format(ResUI.InvalidProperty, "PublicKey")); - } - } - - if (item.Network == nameof(ETransport.xhttp) - && !item.Extra.IsNullOrEmpty()) - { - // check xhttp extra json validity - var xhttpExtra = JsonUtils.ParseJson(item.Extra); - if (xhttpExtra is null) - { - errors.Add(string.Format(ResUI.InvalidProperty, "XHTTP Extra")); - } - } - - return errors; - } - - private async Task> ValidateGroupNode(ProfileItem item, ECoreType? coreType = null) - { - var errors = new List(); - - var hasCycle = await GroupProfileManager.HasCycle(item.IndexId, item.GetProtocolExtra()); - if (hasCycle) - { - errors.Add(string.Format(ResUI.GroupSelfReference, item.Remarks)); - return errors; - } - - var (childItems, _) = await GroupProfileManager.GetChildProfileItems(item); - - foreach (var childItem in childItems) - { - var childErrors = new List(); - - if (childItem is null) - { - childErrors.Add(string.Format(ResUI.NodeTagNotExist, "")); - 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.Select(s => s.Insert(0, $"{childItem.Remarks}: "))); - } - return errors; - } - - private static string? ValidateSingboxTransport(EConfigType configType, string net) - { - // sing-box does not support xhttp / kcp transports - if (SingboxUnsupportedTransports.Contains(net)) - { - return string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net); - } - - // sing-box does not support non-tcp transports for protocols other than vmess/trojan/vless/shadowsocks - if (!SingboxTransportSupportedProtocols.Contains(configType) && net != nameof(ETransport.tcp)) - { - return string.Format(ResUI.CoreNotSupportProtocolTransport, - nameof(ECoreType.sing_box), configType.ToString(), net); - } - - // sing-box shadowsocks only supports tcp/ws/quic transports - if (configType == EConfigType.Shadowsocks && !SingboxShadowsocksAllowedTransports.Contains(net)) - { - return string.Format(ResUI.CoreNotSupportProtocolTransport, - nameof(ECoreType.sing_box), configType.ToString(), net); - } - - return null; - } - - 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 + $"{node.Remarks}: " + 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(AppManager.Instance.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 + $"{tagItem.Remarks}: " + s)); - } - - return errors; - } -} diff --git a/v2rayN/ServiceLib/Manager/CoreManager.cs b/v2rayN/ServiceLib/Manager/CoreManager.cs index 159af85e..cd30450f 100644 --- a/v2rayN/ServiceLib/Manager/CoreManager.cs +++ b/v2rayN/ServiceLib/Manager/CoreManager.cs @@ -57,26 +57,27 @@ public class CoreManager } } - public async Task LoadCore(ProfileItem? node) + public async Task LoadCore(CoreConfigContext? context) { - if (node == null) + if (context == null) { await UpdateFunc(false, ResUI.CheckServerSettings); return; } + var contextMod = context; + var node = contextMod.Node; var fileName = Utils.GetBinConfigPath(Global.CoreConfigFileName); - var context = await CoreConfigHandler.BuildCoreConfigContext(_config, node); - var preContext = ConfigHandler.GetPreSocksCoreConfigContext(context); + var preContext = ConfigHandler.GetPreSocksCoreConfigContext(contextMod); if (preContext is not null) { - context = context with + contextMod = contextMod with { TunProtectSsPort = preContext.TunProtectSsPort, ProxyRelaySsPort = preContext.ProxyRelaySsPort, }; } - var result = await CoreConfigHandler.GenerateClientConfig(context, fileName); + var result = await CoreConfigHandler.GenerateClientConfig(contextMod, fileName); if (result.Success != true) { await UpdateFunc(true, result.Msg); @@ -95,7 +96,7 @@ public class CoreManager await WindowsUtils.RemoveTunDevice(); } - await CoreStart(context); + await CoreStart(contextMod); await CoreStartPreService(preContext); if (_processService != null) { @@ -132,7 +133,7 @@ public class CoreManager var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false)); var configPath = Utils.GetBinConfigPath(fileName); - var context = await CoreConfigHandler.BuildCoreConfigContext(_config, node); + var (context, _) = await CoreConfigContextBuilder.Build(_config, node); var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, context, testItem, configPath); if (result.Success != true) { diff --git a/v2rayN/ServiceLib/Manager/GroupProfileManager.cs b/v2rayN/ServiceLib/Manager/GroupProfileManager.cs index 5e87f0e6..510667dd 100644 --- a/v2rayN/ServiceLib/Manager/GroupProfileManager.cs +++ b/v2rayN/ServiceLib/Manager/GroupProfileManager.cs @@ -143,26 +143,27 @@ public class GroupProfileManager .ToList() ?? []; } - public static async Task> GetAllChildProfileItems(ProfileItem profileItem) + public static async Task> GetAllChildProfileItems(ProfileItem profileItem) { - var allChildItems = new List(); + var itemMap = new Dictionary(); var visited = new HashSet(); - await CollectChildItems(profileItem, allChildItems, visited); + await CollectChildItems(profileItem, itemMap, visited); - return allChildItems; + return itemMap; } - private static async Task CollectChildItems(ProfileItem profileItem, List allChildItems, HashSet visited) + private static async Task CollectChildItems(ProfileItem profileItem, Dictionary itemMap, + HashSet visited) { var (childItems, _) = await GetChildProfileItems(profileItem); foreach (var child in childItems.Where(child => visited.Add(child.IndexId))) { - allChildItems.Add(child); + itemMap[child.IndexId] = child; if (child.ConfigType.IsGroupType()) { - await CollectChildItems(child, allChildItems, visited); + await CollectChildItems(child, itemMap, visited); } } } diff --git a/v2rayN/ServiceLib/Models/CoreConfigContext.cs b/v2rayN/ServiceLib/Models/CoreConfigContext.cs index a0a43a20..a9a49f96 100644 --- a/v2rayN/ServiceLib/Models/CoreConfigContext.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigContext.cs @@ -3,6 +3,7 @@ namespace ServiceLib.Models; public record CoreConfigContext { public required ProfileItem Node { get; init; } + public required ECoreType RunCoreType { get; init; } public RoutingItem? RoutingItem { get; init; } public DNSItem? RawDnsItem { get; init; } public SimpleDNSItem SimpleDnsItem { get; init; } = new(); diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 8f798007..5e35d3bb 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -132,33 +132,6 @@ 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. 的本地化字符串。 /// @@ -312,24 +285,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Group '{0}' is empty. Please add at least one node. 的本地化字符串。 - /// - public static string GroupEmpty { - get { - return ResourceManager.GetString("GroupEmpty", resourceCulture); - } - } - - /// - /// 查找类似 {0} Group cannot reference itself or have a circular reference 的本地化字符串。 - /// - public static string GroupSelfReference { - get { - return ResourceManager.GetString("GroupSelfReference", resourceCulture); - } - } - /// /// 查找类似 This is not the correct configuration, please check 的本地化字符串。 /// @@ -357,15 +312,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 The {0} property is invalid, please check. 的本地化字符串。 - /// - public static string InvalidProperty { - get { - return ResourceManager.GetString("InvalidProperty", resourceCulture); - } - } - /// /// 查找类似 Invalid address (URL) 的本地化字符串。 /// @@ -1914,6 +1860,33 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Core '{0}' does not support network type '{1}' 的本地化字符串。 + /// + public static string MsgCoreNotSupportNetwork { + get { + return ResourceManager.GetString("MsgCoreNotSupportNetwork", resourceCulture); + } + } + + /// + /// 查找类似 Core '{0}' does not support protocol '{1}' 的本地化字符串。 + /// + public static string MsgCoreNotSupportProtocol { + get { + return ResourceManager.GetString("MsgCoreNotSupportProtocol", resourceCulture); + } + } + + /// + /// 查找类似 Core '{0}' does not support protocol '{1}' when using transport '{2}' 的本地化字符串。 + /// + public static string MsgCoreNotSupportProtocolTransport { + get { + return ResourceManager.GetString("MsgCoreNotSupportProtocolTransport", resourceCulture); + } + } + /// /// 查找类似 Downloaded GeoFile: {0} successfully 的本地化字符串。 /// @@ -1959,6 +1932,60 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Group {0} child group node {1} error: {2}. Skipping this node. 的本地化字符串。 + /// + public static string MsgGroupChildGroupNodeError { + get { + return ResourceManager.GetString("MsgGroupChildGroupNodeError", resourceCulture); + } + } + + /// + /// 查找类似 Group {0} child group node {1} warning: {2} 的本地化字符串。 + /// + public static string MsgGroupChildGroupNodeWarning { + get { + return ResourceManager.GetString("MsgGroupChildGroupNodeWarning", resourceCulture); + } + } + + /// + /// 查找类似 Group {0} child node {1} error: {2}. Skipping this node. 的本地化字符串。 + /// + public static string MsgGroupChildNodeError { + get { + return ResourceManager.GetString("MsgGroupChildNodeError", resourceCulture); + } + } + + /// + /// 查找类似 Group {0} child node {1} warning: {2} 的本地化字符串。 + /// + public static string MsgGroupChildNodeWarning { + get { + return ResourceManager.GetString("MsgGroupChildNodeWarning", resourceCulture); + } + } + + /// + /// 查找类似 Group {0} has a cycle dependency on child node {1}. Skipping this node. 的本地化字符串。 + /// + public static string MsgGroupCycleDependency { + get { + return ResourceManager.GetString("MsgGroupCycleDependency", resourceCulture); + } + } + + /// + /// 查找类似 Group {0} has no valid child node. 的本地化字符串。 + /// + public static string MsgGroupNoValidChildNode { + get { + return ResourceManager.GetString("MsgGroupNoValidChildNode", resourceCulture); + } + } + /// /// 查找类似 Information 的本地化字符串。 /// @@ -1968,6 +1995,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 The {0} property is invalid, please check 的本地化字符串。 + /// + public static string MsgInvalidProperty { + get { + return ResourceManager.GetString("MsgInvalidProperty", resourceCulture); + } + } + /// /// 查找类似 Please enter the URL 的本地化字符串。 /// @@ -1977,6 +2013,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Not support protocol '{0}' 的本地化字符串。 + /// + public static string MsgNotSupportProtocol { + get { + return ResourceManager.GetString("MsgNotSupportProtocol", resourceCulture); + } + } + /// /// 查找类似 No valid subscriptions set 的本地化字符串。 /// @@ -1995,6 +2040,42 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Routing rule {0} has an empty outbound tag. Fallback to proxy node only. 的本地化字符串。 + /// + public static string MsgRoutingRuleEmptyOutboundTag { + get { + return ResourceManager.GetString("MsgRoutingRuleEmptyOutboundTag", resourceCulture); + } + } + + /// + /// 查找类似 Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. 的本地化字符串。 + /// + public static string MsgRoutingRuleOutboundNodeError { + get { + return ResourceManager.GetString("MsgRoutingRuleOutboundNodeError", resourceCulture); + } + } + + /// + /// 查找类似 Routing rule {0} outbound node {1} not found. Fallback to proxy node only. 的本地化字符串。 + /// + public static string MsgRoutingRuleOutboundNodeNotFound { + get { + return ResourceManager.GetString("MsgRoutingRuleOutboundNodeNotFound", resourceCulture); + } + } + + /// + /// 查找类似 Routing rule {0} outbound node {1} warning: {2} 的本地化字符串。 + /// + public static string MsgRoutingRuleOutboundNodeWarning { + get { + return ResourceManager.GetString("MsgRoutingRuleOutboundNodeWarning", resourceCulture); + } + } + /// /// 查找类似 Filter, press Enter to execute 的本地化字符串。 /// @@ -2049,6 +2130,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Subscription next proxy {0} not found. Skipping. 的本地化字符串。 + /// + public static string MsgSubscriptionNextProfileNotFound { + get { + return ResourceManager.GetString("MsgSubscriptionNextProfileNotFound", resourceCulture); + } + } + + /// + /// 查找类似 Subscription previous proxy {0} not found. Skipping. 的本地化字符串。 + /// + public static string MsgSubscriptionPrevProfileNotFound { + get { + return ResourceManager.GetString("MsgSubscriptionPrevProfileNotFound", resourceCulture); + } + } + /// /// 查找类似 Unpacking... 的本地化字符串。 /// @@ -2103,15 +2202,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Node alias '{0}' does not exist. 的本地化字符串。 - /// - public static string NodeTagNotExist { - get { - return ResourceManager.GetString("NodeTagNotExist", resourceCulture); - } - } - /// /// 查找类似 Non-VMess or SS protocol 的本地化字符串。 /// @@ -2139,15 +2229,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Not support protocol '{0}'. 的本地化字符串。 - /// - public static string NotSupportProtocol { - get { - return ResourceManager.GetString("NotSupportProtocol", resourceCulture); - } - } - /// /// 查找类似 Scan completed, no valid QR code found 的本地化字符串。 /// @@ -2229,24 +2310,6 @@ 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} 的本地化字符串。 /// @@ -2310,15 +2373,6 @@ 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 9aba2fed..0df75d77 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1539,38 +1539,20 @@ Multi-Configuration Fallback by Xray - - Core '{0}' does not support network type '{1}'. + + 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}' when using transport '{2}' - - Core '{0}' does not support protocol '{1}'. + + Core '{0}' does not support protocol '{1}' - - Proxy chained: + + The {0} property is invalid, please check - - Routing rule outbound: - - - Policy group: - - - Node alias '{0}' does not exist. - - - Group '{0}' is empty. Please add at least one node. - - - The {0} property is invalid, please check. - - - {0} Group cannot reference itself or have a circular reference - - - Not support protocol '{0}'. + + Not support protocol '{0}' If the system does not have a tray function, please do not enable it @@ -1668,4 +1650,40 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Routing rule {0} outbound node {1} warning: {2} + + + Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + + + Group {0} has a cycle dependency on child node {1}. Skipping this node. + + + Group {0} child node {1} warning: {2} + + + Group {0} child node {1} error: {2}. Skipping this node. + + + Group {0} child group node {1} warning: {2} + + + Group {0} child group node {1} error: {2}. Skipping this node. + + + Group {0} has no valid child node. + + + Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + + + Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + + + Subscription previous proxy {0} not found. Skipping. + + + Subscription next proxy {0} not found. Skipping. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index 3dfbcc09..6c882112 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1536,38 +1536,20 @@ Xray basculement (multi-sélection) - - Le cœur « {0} » ne prend pas en charge le type de réseau « {1} ». + + Le cœur « {0} » ne prend pas en charge le type de réseau « {1} » - - Le cœur « {0} » ne prend pas en charge le protocole « {1} » avec le mode de transport « {2} ». + + Le cœur « {0} » ne prend pas en charge le protocole « {1} » avec le mode de transport « {2} » - - Le cœur « {0} » ne prend pas en charge le protocole « {1} ». + + Le cœur « {0} » ne prend pas en charge le protocole « {1} » - - Chaîne de proxy : - - - Règle de routage sortante : - - - Groupe de stratégie : - - - L’alias « {0} » n’existe pas. - - - Le groupe « {0} » est vide. Veuillez ajouter au moins une configuration. - - + La propriété {0} est invalide, veuillez vérifier - - Le groupe {0} ne peut pas se référencer lui-même ni créer de référence circulaire - - - Protocole « {0} » non pris en charge. + + Protocole « {0} » non pris en charge Si le système n’a pas de zone de notif., n’activez pas cette option @@ -1665,4 +1647,40 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Routing rule {0} outbound node {1} warning: {2} + + + Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + + + Group {0} has a cycle dependency on child node {1}. Skipping this node. + + + Group {0} child node {1} warning: {2} + + + Group {0} child node {1} error: {2}. Skipping this node. + + + Group {0} child group node {1} warning: {2} + + + Group {0} child group node {1} error: {2}. Skipping this node. + + + Group {0} has no valid child node. + + + Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + + + Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + + + Subscription previous proxy {0} not found. Skipping. + + + Subscription next proxy {0} not found. Skipping. + diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index e69a0a54..dd7d22f4 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1539,38 +1539,20 @@ Multi-Configuration Fallback by Xray - - Core '{0}' does not support network type '{1}'. + + 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}' when using transport '{2}' - - Core '{0}' does not support protocol '{1}'. + + Core '{0}' does not support protocol '{1}' - - Proxy chained: + + The {0} property is invalid, please check - - Routing rule outbound: - - - Policy group: - - - Node alias '{0}' does not exist. - - - Group '{0}' is empty. Please add at least one node. - - - The {0} property is invalid, please check. - - - {0} Group cannot reference itself or have a circular reference - - - Not support protocol '{0}'. + + Not support protocol '{0}' If the system does not have a tray function, please do not enable it @@ -1668,4 +1650,40 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Routing rule {0} outbound node {1} warning: {2} + + + Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + + + Group {0} has a cycle dependency on child node {1}. Skipping this node. + + + Group {0} child node {1} warning: {2} + + + Group {0} child node {1} error: {2}. Skipping this node. + + + Group {0} child group node {1} warning: {2} + + + Group {0} child group node {1} error: {2}. Skipping this node. + + + Group {0} has no valid child node. + + + Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + + + Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + + + Subscription previous proxy {0} not found. Skipping. + + + Subscription next proxy {0} not found. Skipping. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index cc304154..fa53ccc8 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1539,38 +1539,20 @@ Fallback by Xray - - Core '{0}' does not support network type '{1}'. + + 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}' when using transport '{2}' - - Core '{0}' does not support protocol '{1}'. + + Core '{0}' does not support protocol '{1}' - - Proxy chained: + + The {0} property is invalid, please check - - Routing rule outbound: - - - Policy group: - - - Node alias '{0}' does not exist. - - - Group '{0}' is empty. Please add at least one node. - - - The {0} property is invalid, please check. - - - {0} Group cannot reference itself or have a circular reference - - - Not support protocol '{0}'. + + Not support protocol '{0}' If the system does not have a tray function, please do not enable it @@ -1668,4 +1650,40 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Routing rule {0} outbound node {1} warning: {2} + + + Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + + + Group {0} has a cycle dependency on child node {1}. Skipping this node. + + + Group {0} child node {1} warning: {2} + + + Group {0} child node {1} error: {2}. Skipping this node. + + + Group {0} child group node {1} warning: {2} + + + Group {0} child group node {1} error: {2}. Skipping this node. + + + Group {0} has no valid child node. + + + Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + + + Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + + + Subscription previous proxy {0} not found. Skipping. + + + Subscription next proxy {0} not found. Skipping. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 6f093f72..b33a10e3 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1539,38 +1539,20 @@ Multi-Configuration Fallback by Xray - - Core '{0}' does not support network type '{1}'. + + 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}' when using transport '{2}' - - Core '{0}' does not support protocol '{1}'. + + Core '{0}' does not support protocol '{1}' - - Proxy chained: + + The {0} property is invalid, please check - - Routing rule outbound: - - - Policy group: - - - Node alias '{0}' does not exist. - - - Group '{0}' is empty. Please add at least one node. - - - The {0} property is invalid, please check. - - - {0} Group cannot reference itself or have a circular reference - - - Not support protocol '{0}'. + + Not support protocol '{0}' If the system does not have a tray function, please do not enable it @@ -1668,4 +1650,40 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Routing rule {0} outbound node {1} warning: {2} + + + Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + + + Group {0} has a cycle dependency on child node {1}. Skipping this node. + + + Group {0} child node {1} warning: {2} + + + Group {0} child node {1} error: {2}. Skipping this node. + + + Group {0} child group node {1} warning: {2} + + + Group {0} child group node {1} error: {2}. Skipping this node. + + + Group {0} has no valid child node. + + + Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + + + Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + + + Subscription previous proxy {0} not found. Skipping. + + + Subscription next proxy {0} not found. Skipping. + \ 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 c0f8665d..be76525e 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1536,38 +1536,20 @@ 多选故障转移 Xray - - 核心 '{0}' 不支持网络类型 '{1}'。 + + 核心 '{0}' 不支持网络类型 '{1}' - - 核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}'。 + + 核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}' - - 核心 '{0}' 不支持协议 '{1}'。 + + 核心 '{0}' 不支持协议 '{1}' - - 代理链: + + {0} 属性无效,请检查 - - 路由规则出站: - - - 策略组: - - - 别名 '{0}' 不存在。 - - - 组“{0}”为空。请至少添加一个配置。 - - - {0}属性无效,请检查 - - - {0} 分组不能引用自身或循环引用 - - - 不支持协议 '{0}'。 + + 不支持协议 '{0}' 如果系统没有托盘功能,请不要开启 @@ -1665,4 +1647,40 @@ Finalmask + + 路由规则 {0} 出站节点 {1} 警告:{2} + + + 路由规则 {0} 出站节点 {1} 错误:{2}。已回退为仅使用代理节点。 + + + 节点组 {0} 与子节点 {1} 存在循环依赖,已跳过该节点。 + + + 节点组 {0} 子节点 {1} 警告:{2} + + + 节点组 {0} 子节点 {1} 错误:{2}。已跳过该节点。 + + + 节点组 {0} 子节点组 {1} 警告:{2} + + + 节点组 {0} 子节点组 {1} 错误:{2}。已跳过该节点。 + + + 节点组 {0} 下没有有效的子节点。 + + + 路由规则 {0} 的出站标签为空,已回退为仅使用代理节点。 + + + 路由规则 {0} 的出站节点 {1} 未找到,已回退为仅使用代理节点。 + + + 订阅前置节点 {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 9002c3af..4132a27b 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1536,38 +1536,20 @@ 多選容錯移轉 Xray - - 核心 '{0}' 不支援網路類型 '{1}'. + + 核心 '{0}' 不支援網路類型 '{1}' - - 核心 '{0}' 在使用傳輸方式 '{2}' 時不支援協定 '{1}'. + + 核心 '{0}' 在使用傳輸方式 '{2}' 時不支援協定 '{1}' - - 核心 '{0}' 不支援協定 '{1}'. + + 核心 '{0}' 不支援協定 '{1}' - - 代理鏈: + + {0} 屬性無效,請檢查 - - 路由規則出站: - - - 策略組: - - - 別名 '{0}' 不存在。 - - - 組“{0}”為空.請至少添加一個配置。 - - - {0}屬性無效,請檢查 - - - {0} 分組不能引用自身或循環引用 - - - 不支援協定 '{0}'. + + 不支援協定 '{0}' 如果系統沒有託盤功能,請不要開啟 @@ -1665,4 +1647,40 @@ Finalmask + + Routing rule {0} outbound node {1} warning: {2} + + + Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + + + Group {0} has a cycle dependency on child node {1}. Skipping this node. + + + Group {0} child node {1} warning: {2} + + + Group {0} child node {1} error: {2}. Skipping this node. + + + Group {0} child group node {1} warning: {2} + + + Group {0} child group node {1} error: {2}. Skipping this node. + + + Group {0} has no valid child node. + + + Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + + + Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + + + Subscription previous proxy {0} not found. Skipping. + + + Subscription next proxy {0} not found. Skipping. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs index 6e4d021c..d30917ec 100644 --- a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs @@ -234,13 +234,6 @@ public class AddGroupServerViewModel : MyReactiveObject SelectedSource.SetProtocolExtra(protocolExtra); - var hasCycle = await GroupProfileManager.HasCycle(SelectedSource.IndexId, protocolExtra); - if (hasCycle) - { - NoticeManager.Instance.Enqueue(string.Format(ResUI.GroupSelfReference, remarks)); - return; - } - if (await ConfigHandler.AddServerCommon(_config, SelectedSource) == 0) { NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index 8f5e9029..becabf25 100644 --- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -540,7 +540,14 @@ public class MainWindowViewModel : MyReactiveObject { SetReloadEnabled(false); - var msgs = await ActionPrecheckManager.Instance.Check(_config.IndexId); + var profileItem = await ConfigHandler.GetDefaultServer(_config); + if (profileItem == null) + { + NoticeManager.Instance.Enqueue(ResUI.CheckServerSettings); + return; + } + var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, profileItem); + var msgs = new List([..validatorResult.Errors, ..validatorResult.Warnings]); if (msgs.Count > 0) { foreach (var msg in msgs) @@ -548,12 +555,15 @@ public class MainWindowViewModel : MyReactiveObject NoticeManager.Instance.SendMessage(msg); } NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true)); - return; + if (!validatorResult.Success) + { + return; + } } await Task.Run(async () => { - await LoadCore(); + await LoadCore(context); await SysProxyHandler.UpdateSysProxy(_config, false); await Task.Delay(1000); }); @@ -594,10 +604,9 @@ public class MainWindowViewModel : MyReactiveObject RxApp.MainThreadScheduler.Schedule(() => BlReloadEnabled = enabled); } - private async Task LoadCore() + private async Task LoadCore(CoreConfigContext? context) { - var node = await ConfigHandler.GetDefaultServer(_config); - await CoreManager.Instance.LoadCore(node); + await CoreManager.Instance.LoadCore(context); } #endregion core job diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 8b4e7eb0..1cc6f46e 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -788,7 +788,8 @@ public class ProfilesViewModel : MyReactiveObject return; } - var msgs = await ActionPrecheckManager.Instance.Check(item); + var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, item); + var msgs = new List([..validatorResult.Errors, ..validatorResult.Warnings]); if (msgs.Count > 0) { foreach (var msg in msgs) @@ -796,12 +797,14 @@ public class ProfilesViewModel : MyReactiveObject NoticeManager.Instance.SendMessage(msg); } NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true)); - return; + if (!validatorResult.Success) + { + return; + } } if (blClipboard) { - var context = await CoreConfigHandler.BuildCoreConfigContext(_config, item); var result = await CoreConfigHandler.GenerateClientConfig(context, null); if (result.Success != true) { @@ -825,7 +828,20 @@ public class ProfilesViewModel : MyReactiveObject { return; } - var context = await CoreConfigHandler.BuildCoreConfigContext(_config, item); + var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, item); + var msgs = new List([..validatorResult.Errors, ..validatorResult.Warnings]); + if (msgs.Count > 0) + { + foreach (var msg in msgs) + { + NoticeManager.Instance.SendMessage(msg); + } + NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true)); + if (!validatorResult.Success) + { + return; + } + } var result = await CoreConfigHandler.GenerateClientConfig(context, fileName); if (result.Success != true) {