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..7c1e93f1 --- /dev/null +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -0,0 +1,266 @@ +namespace ServiceLib.Handler.Builder; + +public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult) +{ + public bool Success => ValidatorResult.Success; +} + +public class CoreConfigContextBuilder +{ + public static async Task Build(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, + RunCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType), + 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 FillNodeContext(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 => !Global.OutboundTags.Contains(ruleItem.OutboundTag))) + { + var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag); + if (ruleOutboundNode == null) + { + continue; + } + + var (actRuleNode, ruleNodeValidatorResult) = await FillNodeContext(context, ruleOutboundNode, false); + validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Warnings.Select(w => + $"Routing rule {ruleItem.Remarks} outbound node {ruleItem.OutboundTag} warning: {w}")); + if (!ruleNodeValidatorResult.Success) + { + validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e => + $"Routing rule {ruleItem.Remarks} outbound node {ruleItem.OutboundTag} error: {e}. Fallback to proxy node only.")); + ruleItem.OutboundTag = Global.ProxyTag; + continue; + } + + context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode; + } + } + + return new CoreConfigContextBuilderResult(context, validatorResult); + } + + public static async Task<(ProfileItem, NodeValidatorResult)> FillNodeContext(CoreConfigContext context, + ProfileItem node, + bool includeSubChain = true) + { + if (node.IndexId.IsNullOrEmpty()) + { + return (node, NodeValidatorResult.Empty()); + } + + if (includeSubChain) + { + var virtualChainNode = await BuildVirtualSubChainNode(node); + if (virtualChainNode != null) + { + context.AllProxiesMap[virtualChainNode.IndexId] = virtualChainNode; + return await FillNodeContext(context, virtualChainNode, false); + } + } + + var fillResult = await FillNodeContextPrivate(context, node); + return (node, fillResult); + } + + private static async Task BuildVirtualSubChainNode(ProfileItem node) + { + if (node.Subid.IsNullOrEmpty()) + { + return null; + } + + var subItem = await AppManager.Instance.GetSubItem(node.Subid); + if (subItem == null + || (subItem.PrevProfile.IsNullOrEmpty() + && subItem.NextProfile.IsNullOrEmpty())) + { + return null; + } + + var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); + if (prevNode is null && nextNode is null) + { + return null; + } + + // 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; + } + + private static async Task FillNodeContextPrivate(CoreConfigContext context, ProfileItem node) + { + if (node.ConfigType.IsGroupType()) + { + return await FillGroupNodeContextPrivate(context, node); + } + else + { + return FillNormalNodeContextPrivate(context, node); + } + } + + private static NodeValidatorResult FillNormalNodeContextPrivate(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; + } + + private static async Task FillGroupNodeContextPrivate(CoreConfigContext context, + ProfileItem node) + { + if (!node.ConfigType.IsGroupType()) + { + return NodeValidatorResult.Empty(); + } + + HashSet ancestors = [node.IndexId]; + HashSet globalVisited = [node.IndexId]; + return await FillGroupNodeContextPrivate(context, node, globalVisited, ancestors); + } + + private static async Task FillGroupNodeContextPrivate( + 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( + $"Group {node.Remarks} has a cycle dependency on child node {childNode.Remarks}. Skipping this node."); + continue; + } + + if (globalVisitedGroup.Contains(childNode.IndexId)) + { + childIndexIdList.Add(childNode.IndexId); + continue; + } + + if (!childNode.ConfigType.IsGroupType()) + { + var childNodeResult = FillNormalNodeContextPrivate(context, childNode); + childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w => + $"Group {node.Remarks} child node {childNode.Remarks} warning: {w}")); + childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e => + $"Group {node.Remarks} child node {childNode.Remarks} error: {e}. Skipping this node.")); + if (!childNodeResult.Success) + { + continue; + } + + globalVisitedGroup.Add(childNode.IndexId); + childIndexIdList.Add(childNode.IndexId); + continue; + } + + globalVisitedGroup.Add(childNode.IndexId); + var newAncestorsGroup = new HashSet(ancestorsGroup) { childNode.IndexId }; + var childGroupResult = + await FillGroupNodeContextPrivate(context, childNode, globalVisitedGroup, newAncestorsGroup); + childNodeValidatorResult.Warnings.AddRange(childGroupResult.Warnings.Select(w => + $"Group {node.Remarks} child group node {childNode.Remarks} warning: {w}")); + childNodeValidatorResult.Errors.AddRange(childGroupResult.Errors.Select(e => + $"Group {node.Remarks} child group node {childNode.Remarks} error: {e}. Skipping this node.")); + if (!childGroupResult.Success) + { + continue; + } + + childIndexIdList.Add(childNode.IndexId); + } + + if (childIndexIdList.Count == 0) + { + childNodeValidatorResult.Errors.Add($"Group {node.Remarks} has no valid child node."); + 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..bfffb599 --- /dev/null +++ b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs @@ -0,0 +1,180 @@ +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.InvalidProperty, "Address")); + v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.InvalidProperty, "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.CoreNotSupportProtocol, nameof(ECoreType.sing_box), item.ConfigType)); + } + } + else if (coreType is ECoreType.Xray) + { + if (!Global.XraySupportConfigType.Contains(item.ConfigType)) + { + v.Error(string.Format(ResUI.CoreNotSupportProtocol, 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.InvalidProperty, "Password")); + break; + + case EConfigType.VLESS: + // Example of converting a non-critical issue to Warning if desired + if (item.Password.Length <= 30 && !Utils.IsGuidByParse(item.Password)) + { + v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "Password")); + } + else + { + v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "Password")); + } + + v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty), + string.Format(ResUI.InvalidProperty, "Flow")); + break; + + case EConfigType.Shadowsocks: + v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "Password")); + v.Assert( + !string.IsNullOrEmpty(protocolExtra.SsMethod) && + Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod), + string.Format(ResUI.InvalidProperty, "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.InvalidProperty, "TLS Certificate")); + } + } + + if (item.StreamSecurity == Global.StreamSecurityReality) + { + v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "PublicKey")); + } + + if (item.Network == nameof(ETransport.xhttp) && !item.Extra.IsNullOrEmpty()) + { + if (JsonUtils.ParseJson(item.Extra) is null) + { + v.Error(string.Format(ResUI.InvalidProperty, "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.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; + } +} diff --git a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs index a38faafa..37e1e268 100644 --- a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs @@ -93,13 +93,14 @@ 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 builderResult = await CoreConfigContextBuilder.Build(config, new()); + 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.FillNodeContext(context, node, true); if (node.IndexId == actNode.IndexId) { continue; @@ -146,128 +147,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/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) {