mirror of
https://github.com/2dust/v2rayN.git
synced 2026-02-28 13:13:04 +00:00
Refactor node pre check
This commit is contained in:
parent
7931058342
commit
b4c386238c
10 changed files with 521 additions and 503 deletions
|
|
@ -24,6 +24,7 @@ global using ServiceLib.Common;
|
||||||
global using ServiceLib.Enums;
|
global using ServiceLib.Enums;
|
||||||
global using ServiceLib.Events;
|
global using ServiceLib.Events;
|
||||||
global using ServiceLib.Handler;
|
global using ServiceLib.Handler;
|
||||||
|
global using ServiceLib.Handler.Builder;
|
||||||
global using ServiceLib.Handler.Fmt;
|
global using ServiceLib.Handler.Fmt;
|
||||||
global using ServiceLib.Handler.SysProxy;
|
global using ServiceLib.Handler.SysProxy;
|
||||||
global using ServiceLib.Helper;
|
global using ServiceLib.Helper;
|
||||||
|
|
|
||||||
284
v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs
Normal file
284
v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
namespace ServiceLib.Handler.Builder;
|
||||||
|
|
||||||
|
public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult)
|
||||||
|
{
|
||||||
|
public bool Success => ValidatorResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CoreConfigContextBuilder
|
||||||
|
{
|
||||||
|
public static async Task<CoreConfigContextBuilderResult> 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<List<RulesItem>>(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<ProfileItem?> 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<string?> 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<NodeValidatorResult> 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<NodeValidatorResult> FillGroupNodeContextPrivate(CoreConfigContext context,
|
||||||
|
ProfileItem node)
|
||||||
|
{
|
||||||
|
if (!node.ConfigType.IsGroupType())
|
||||||
|
{
|
||||||
|
return NodeValidatorResult.Empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<string> ancestors = [];
|
||||||
|
HashSet<string> globalVisited = [];
|
||||||
|
return await FillGroupNodeContextPrivate(context, node, globalVisited, ancestors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<NodeValidatorResult> FillGroupNodeContextPrivate(
|
||||||
|
CoreConfigContext context,
|
||||||
|
ProfileItem node,
|
||||||
|
HashSet<string> globalVisitedGroup,
|
||||||
|
HashSet<string> ancestorsGroup)
|
||||||
|
{
|
||||||
|
var (groupChildList, _) = await GroupProfileManager.GetChildProfileItems(node);
|
||||||
|
List<string> childIndexIdList = [];
|
||||||
|
var childNodeValidatorResult = NodeValidatorResult.Empty();
|
||||||
|
foreach (var childNode in groupChildList)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
childIndexIdList.Add(childNode.IndexId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect cycle before recursive call
|
||||||
|
if (DetectCycleFromGroup(childNode, globalVisitedGroup, ancestorsGroup))
|
||||||
|
{
|
||||||
|
childNodeValidatorResult.Errors.Add(
|
||||||
|
$"Group {node.Remarks} has a cycle dependency on child node {childNode.Remarks}. Skipping this node.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalVisitedGroup.Add(childNode.IndexId);
|
||||||
|
var newAncestorsGroup = new HashSet<string>(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DetectCycleFromGroup(
|
||||||
|
ProfileItem item,
|
||||||
|
HashSet<string> globalVisited,
|
||||||
|
HashSet<string> ancestors)
|
||||||
|
{
|
||||||
|
if (!item.ConfigType.IsGroupType())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalVisited.Contains(item.IndexId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ancestors.Contains(item.IndexId);
|
||||||
|
}
|
||||||
|
}
|
||||||
180
v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs
Normal file
180
v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
namespace ServiceLib.Handler.Builder;
|
||||||
|
|
||||||
|
public record NodeValidatorResult(List<string> Errors, List<string> Warnings)
|
||||||
|
{
|
||||||
|
public bool Success => Errors.Count == 0;
|
||||||
|
|
||||||
|
public static NodeValidatorResult Empty()
|
||||||
|
{
|
||||||
|
return new NodeValidatorResult([], []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NodeValidator
|
||||||
|
{
|
||||||
|
// Static validator rules
|
||||||
|
private static readonly HashSet<string> SingboxUnsupportedTransports =
|
||||||
|
[nameof(ETransport.kcp), nameof(ETransport.xhttp)];
|
||||||
|
|
||||||
|
private static readonly HashSet<EConfigType> SingboxTransportSupportedProtocols =
|
||||||
|
[EConfigType.VMess, EConfigType.VLESS, EConfigType.Trojan, EConfigType.Shadowsocks];
|
||||||
|
|
||||||
|
private static readonly HashSet<string> 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<string> Errors { get; } = [];
|
||||||
|
public List<string> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -93,13 +93,14 @@ public static class CoreConfigHandler
|
||||||
public static async Task<RetResult> GenerateClientSpeedtestConfig(Config config, string fileName, List<ServerTestItem> selecteds, ECoreType coreType)
|
public static async Task<RetResult> GenerateClientSpeedtestConfig(Config config, string fileName, List<ServerTestItem> selecteds, ECoreType coreType)
|
||||||
{
|
{
|
||||||
var result = new RetResult();
|
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())
|
var ids = selecteds.Where(serverTestItem => !serverTestItem.IndexId.IsNullOrEmpty())
|
||||||
.Select(serverTestItem => serverTestItem.IndexId);
|
.Select(serverTestItem => serverTestItem.IndexId);
|
||||||
var nodes = await AppManager.Instance.GetProfileItemsByIndexIds(ids);
|
var nodes = await AppManager.Instance.GetProfileItemsByIndexIds(ids);
|
||||||
foreach (var node in nodes)
|
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)
|
if (node.IndexId == actNode.IndexId)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -146,128 +147,4 @@ public static class CoreConfigHandler
|
||||||
await File.WriteAllTextAsync(fileName, result.Data.ToString());
|
await File.WriteAllTextAsync(fileName, result.Data.ToString());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<CoreConfigContext> 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<List<RulesItem>>(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<ProfileItem> FillNodeContext(CoreConfigContext context, ProfileItem node, bool includeSubChain = true)
|
|
||||||
{
|
|
||||||
if (node.IndexId.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
var newItems = new List<ProfileItem> { 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<string?> 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,352 +0,0 @@
|
||||||
namespace ServiceLib.Manager;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.).
|
|
||||||
/// </summary>
|
|
||||||
public class ActionPrecheckManager
|
|
||||||
{
|
|
||||||
private static readonly Lazy<ActionPrecheckManager> _instance = new();
|
|
||||||
public static ActionPrecheckManager Instance => _instance.Value;
|
|
||||||
|
|
||||||
// sing-box supported transports for different protocol types
|
|
||||||
private static readonly HashSet<string> SingboxUnsupportedTransports = [nameof(ETransport.kcp), nameof(ETransport.xhttp)];
|
|
||||||
|
|
||||||
private static readonly HashSet<EConfigType> SingboxTransportSupportedProtocols =
|
|
||||||
[EConfigType.VMess, EConfigType.VLESS, EConfigType.Trojan, EConfigType.Shadowsocks];
|
|
||||||
|
|
||||||
private static readonly HashSet<string> SingboxShadowsocksAllowedTransports =
|
|
||||||
[nameof(ETransport.tcp), nameof(ETransport.ws), nameof(ETransport.quic)];
|
|
||||||
|
|
||||||
public async Task<List<string>> 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<List<string>> Check(ProfileItem? item)
|
|
||||||
{
|
|
||||||
if (item is null)
|
|
||||||
{
|
|
||||||
return [ResUI.PleaseSelectServer];
|
|
||||||
}
|
|
||||||
|
|
||||||
var errors = new List<string>();
|
|
||||||
|
|
||||||
errors.AddRange(await ValidateCurrentNodeAndCoreSupport(item));
|
|
||||||
errors.AddRange(await ValidateRelatedNodesExistAndValid(item));
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<string>> 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<List<string>> ValidateNodeAndCoreSupport(ProfileItem item, ECoreType? coreType = null)
|
|
||||||
{
|
|
||||||
var errors = new List<string>();
|
|
||||||
|
|
||||||
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<List<string>> ValidateNormalNode(ProfileItem item, ECoreType? coreType = null)
|
|
||||||
{
|
|
||||||
var errors = new List<string>();
|
|
||||||
|
|
||||||
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<List<string>> ValidateGroupNode(ProfileItem item, ECoreType? coreType = null)
|
|
||||||
{
|
|
||||||
var errors = new List<string>();
|
|
||||||
|
|
||||||
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<string>();
|
|
||||||
|
|
||||||
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<List<string>> ValidateRelatedNodesExistAndValid(ProfileItem? item)
|
|
||||||
{
|
|
||||||
var errors = new List<string>();
|
|
||||||
errors.AddRange(await ValidateProxyChainedNodeExistAndValid(item));
|
|
||||||
errors.AddRange(await ValidateRoutingNodeExistAndValid(item));
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<string>> ValidateProxyChainedNodeExistAndValid(ProfileItem? item)
|
|
||||||
{
|
|
||||||
var errors = new List<string>();
|
|
||||||
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<string> 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<List<string>> ValidateRoutingNodeExistAndValid(ProfileItem? item)
|
|
||||||
{
|
|
||||||
var errors = new List<string>();
|
|
||||||
|
|
||||||
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<List<RulesItem>>(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
await UpdateFunc(false, ResUI.CheckServerSettings);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var contextMod = context;
|
||||||
|
var node = contextMod.Node;
|
||||||
var fileName = Utils.GetBinConfigPath(Global.CoreConfigFileName);
|
var fileName = Utils.GetBinConfigPath(Global.CoreConfigFileName);
|
||||||
var context = await CoreConfigHandler.BuildCoreConfigContext(_config, node);
|
var preContext = ConfigHandler.GetPreSocksCoreConfigContext(contextMod);
|
||||||
var preContext = ConfigHandler.GetPreSocksCoreConfigContext(context);
|
|
||||||
if (preContext is not null)
|
if (preContext is not null)
|
||||||
{
|
{
|
||||||
context = context with
|
contextMod = contextMod with
|
||||||
{
|
{
|
||||||
TunProtectSsPort = preContext.TunProtectSsPort,
|
TunProtectSsPort = preContext.TunProtectSsPort,
|
||||||
ProxyRelaySsPort = preContext.ProxyRelaySsPort,
|
ProxyRelaySsPort = preContext.ProxyRelaySsPort,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
var result = await CoreConfigHandler.GenerateClientConfig(context, fileName);
|
var result = await CoreConfigHandler.GenerateClientConfig(contextMod, fileName);
|
||||||
if (result.Success != true)
|
if (result.Success != true)
|
||||||
{
|
{
|
||||||
await UpdateFunc(true, result.Msg);
|
await UpdateFunc(true, result.Msg);
|
||||||
|
|
@ -95,7 +96,7 @@ public class CoreManager
|
||||||
await WindowsUtils.RemoveTunDevice();
|
await WindowsUtils.RemoveTunDevice();
|
||||||
}
|
}
|
||||||
|
|
||||||
await CoreStart(context);
|
await CoreStart(contextMod);
|
||||||
await CoreStartPreService(preContext);
|
await CoreStartPreService(preContext);
|
||||||
if (_processService != null)
|
if (_processService != null)
|
||||||
{
|
{
|
||||||
|
|
@ -132,7 +133,7 @@ public class CoreManager
|
||||||
|
|
||||||
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
|
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
|
||||||
var configPath = Utils.GetBinConfigPath(fileName);
|
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);
|
var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, context, testItem, configPath);
|
||||||
if (result.Success != true)
|
if (result.Success != true)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -143,26 +143,27 @@ public class GroupProfileManager
|
||||||
.ToList() ?? [];
|
.ToList() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<List<ProfileItem>> GetAllChildProfileItems(ProfileItem profileItem)
|
public static async Task<Dictionary<string, ProfileItem>> GetAllChildProfileItems(ProfileItem profileItem)
|
||||||
{
|
{
|
||||||
var allChildItems = new List<ProfileItem>();
|
var itemMap = new Dictionary<string, ProfileItem>();
|
||||||
var visited = new HashSet<string>();
|
var visited = new HashSet<string>();
|
||||||
|
|
||||||
await CollectChildItems(profileItem, allChildItems, visited);
|
await CollectChildItems(profileItem, itemMap, visited);
|
||||||
|
|
||||||
return allChildItems;
|
return itemMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task CollectChildItems(ProfileItem profileItem, List<ProfileItem> allChildItems, HashSet<string> visited)
|
private static async Task CollectChildItems(ProfileItem profileItem, Dictionary<string, ProfileItem> itemMap,
|
||||||
|
HashSet<string> visited)
|
||||||
{
|
{
|
||||||
var (childItems, _) = await GetChildProfileItems(profileItem);
|
var (childItems, _) = await GetChildProfileItems(profileItem);
|
||||||
foreach (var child in childItems.Where(child => visited.Add(child.IndexId)))
|
foreach (var child in childItems.Where(child => visited.Add(child.IndexId)))
|
||||||
{
|
{
|
||||||
allChildItems.Add(child);
|
itemMap[child.IndexId] = child;
|
||||||
|
|
||||||
if (child.ConfigType.IsGroupType())
|
if (child.ConfigType.IsGroupType())
|
||||||
{
|
{
|
||||||
await CollectChildItems(child, allChildItems, visited);
|
await CollectChildItems(child, itemMap, visited);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ namespace ServiceLib.Models;
|
||||||
public record CoreConfigContext
|
public record CoreConfigContext
|
||||||
{
|
{
|
||||||
public required ProfileItem Node { get; init; }
|
public required ProfileItem Node { get; init; }
|
||||||
|
public required ECoreType RunCoreType { get; init; }
|
||||||
public RoutingItem? RoutingItem { get; init; }
|
public RoutingItem? RoutingItem { get; init; }
|
||||||
public DNSItem? RawDnsItem { get; init; }
|
public DNSItem? RawDnsItem { get; init; }
|
||||||
public SimpleDNSItem SimpleDnsItem { get; init; } = new();
|
public SimpleDNSItem SimpleDnsItem { get; init; } = new();
|
||||||
|
|
|
||||||
|
|
@ -540,7 +540,14 @@ public class MainWindowViewModel : MyReactiveObject
|
||||||
{
|
{
|
||||||
SetReloadEnabled(false);
|
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<string>([..validatorResult.Errors, ..validatorResult.Warnings]);
|
||||||
if (msgs.Count > 0)
|
if (msgs.Count > 0)
|
||||||
{
|
{
|
||||||
foreach (var msg in msgs)
|
foreach (var msg in msgs)
|
||||||
|
|
@ -548,12 +555,15 @@ public class MainWindowViewModel : MyReactiveObject
|
||||||
NoticeManager.Instance.SendMessage(msg);
|
NoticeManager.Instance.SendMessage(msg);
|
||||||
}
|
}
|
||||||
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
|
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
|
||||||
|
if (!validatorResult.Success)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Task.Run(async () =>
|
await Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await LoadCore();
|
await LoadCore(context);
|
||||||
await SysProxyHandler.UpdateSysProxy(_config, false);
|
await SysProxyHandler.UpdateSysProxy(_config, false);
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
});
|
});
|
||||||
|
|
@ -594,10 +604,9 @@ public class MainWindowViewModel : MyReactiveObject
|
||||||
RxApp.MainThreadScheduler.Schedule(() => BlReloadEnabled = enabled);
|
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(context);
|
||||||
await CoreManager.Instance.LoadCore(node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion core job
|
#endregion core job
|
||||||
|
|
|
||||||
|
|
@ -788,7 +788,8 @@ public class ProfilesViewModel : MyReactiveObject
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var msgs = await ActionPrecheckManager.Instance.Check(item);
|
var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, item);
|
||||||
|
var msgs = new List<string>([..validatorResult.Errors, ..validatorResult.Warnings]);
|
||||||
if (msgs.Count > 0)
|
if (msgs.Count > 0)
|
||||||
{
|
{
|
||||||
foreach (var msg in msgs)
|
foreach (var msg in msgs)
|
||||||
|
|
@ -796,12 +797,14 @@ public class ProfilesViewModel : MyReactiveObject
|
||||||
NoticeManager.Instance.SendMessage(msg);
|
NoticeManager.Instance.SendMessage(msg);
|
||||||
}
|
}
|
||||||
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
|
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
|
||||||
|
if (!validatorResult.Success)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (blClipboard)
|
if (blClipboard)
|
||||||
{
|
{
|
||||||
var context = await CoreConfigHandler.BuildCoreConfigContext(_config, item);
|
|
||||||
var result = await CoreConfigHandler.GenerateClientConfig(context, null);
|
var result = await CoreConfigHandler.GenerateClientConfig(context, null);
|
||||||
if (result.Success != true)
|
if (result.Success != true)
|
||||||
{
|
{
|
||||||
|
|
@ -825,7 +828,20 @@ public class ProfilesViewModel : MyReactiveObject
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var context = await CoreConfigHandler.BuildCoreConfigContext(_config, item);
|
var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, item);
|
||||||
|
var msgs = new List<string>([..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);
|
var result = await CoreConfigHandler.GenerateClientConfig(context, fileName);
|
||||||
if (result.Success != true)
|
if (result.Success != true)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue