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; } }