Optimize proxy chain handling for multiple nodes (#7468)

* Improves outbound proxy chain handling.

* Improves sing-box outbound proxy chain handling.

* AI-optimized code
This commit is contained in:
DHR60 2025-06-21 16:45:17 +08:00 committed by GitHub
parent a46a4ad7c1
commit fefa7ded5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 363 additions and 35 deletions

View file

@ -336,7 +336,7 @@ public class CoreConfigSingboxService
await GenExperimental(singboxConfig);
singboxConfig.outbounds.RemoveAt(0);
var tagProxy = new List<string>();
var proxyProfiles = new List<ProfileItem>();
foreach (var it in selecteds)
{
if (it.ConfigType == EConfigType.Custom)
@ -370,42 +370,18 @@ public class CoreConfigSingboxService
}
//outbound
var outbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
await GenOutbound(item, outbound);
outbound.tag = $"{Global.ProxyTag}-{tagProxy.Count + 1}";
singboxConfig.outbounds.Insert(0, outbound);
tagProxy.Add(outbound.tag);
proxyProfiles.Add(item);
}
if (tagProxy.Count <= 0)
if (proxyProfiles.Count <= 0)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenOutboundsList(proxyProfiles, singboxConfig);
await GenDns(null, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);
//add urltest outbound
var outUrltest = new Outbound4Sbox
{
type = "urltest",
tag = $"{Global.ProxyTag}-auto",
outbounds = tagProxy,
interrupt_exist_connections = false,
};
singboxConfig.outbounds.Insert(0, outUrltest);
//add selector outbound
var outSelector = new Outbound4Sbox
{
type = "selector",
tag = Global.ProxyTag,
outbounds = JsonUtils.DeepCopy(tagProxy),
interrupt_exist_connections = false,
};
outSelector.outbounds.Insert(0, outUrltest.tag);
singboxConfig.outbounds.Insert(0, outSelector);
ret.Success = true;
ret.Data = JsonUtils.Serialize(singboxConfig);
return ret;
@ -974,6 +950,179 @@ public class CoreConfigSingboxService
return 0;
}
private async Task<int> GenOutboundsList(List<ProfileItem> nodes, SingboxConfig singboxConfig)
{
try
{
// Get outbound template and initialize lists
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
if (txtOutbound.IsNullOrEmpty())
{
return 0;
}
var resultOutbounds = new List<Outbound4Sbox>();
var prevOutbounds = new List<Outbound4Sbox>(); // Separate list for prev outbounds
var proxyTags = new List<string>(); // For selector and urltest outbounds
// Cache for chain proxies to avoid duplicate generation
var chainProxyCache = new Dictionary<string, (string?, Outbound4Sbox?)>();
var prevProxyTags = new Dictionary<string, string>(); // Map from profile name to tag
int prevIndex = 0; // Index for prev outbounds
// Process each node
int index = 0;
foreach (var node in nodes)
{
index++;
// Skip unsupported config types
if (node.ConfigType is EConfigType.Custom)
{
continue;
}
// Handle proxy chain
string? prevTag = null;
Outbound4Sbox? nextOutbound = null;
if (node.Subid.IsNotEmpty())
{
// Check if chain proxy is already cached
if (chainProxyCache.TryGetValue(node.Subid, out var chainProxy))
{
prevTag = chainProxy.Item1;
nextOutbound = chainProxy.Item2;
}
else
{
// Generate chain proxy and cache it
var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
if (subItem != null)
{
// Process previous proxy
if (!subItem.PrevProfile.IsNullOrEmpty())
{
// Check if this previous proxy was already created
if (prevProxyTags.TryGetValue(subItem.PrevProfile, out var existingTag))
{
prevTag = existingTag;
}
else
{
var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
if (prevNode != null && prevNode.ConfigType != EConfigType.Custom)
{
prevIndex++;
var prevOutbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
prevTag = $"{Global.ProxyTag}-prev-{prevIndex}";
prevOutbound.tag = prevTag;
prevProxyTags[subItem.PrevProfile] = prevTag;
// Add to prev outbounds list (will be added at the end)
prevOutbounds.Add(prevOutbound);
}
}
}
// Process next proxy
var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode != null && nextNode.ConfigType != EConfigType.Custom)
{
nextOutbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
await GenOutbound(nextNode, nextOutbound);
}
// Cache the chain proxy
chainProxyCache[node.Subid] = (prevTag, nextOutbound);
}
}
}
// Create main outbound
var outbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
await GenOutbound(node, outbound);
outbound.tag = $"{Global.ProxyTag}-{index}";
// Configure proxy chain relationships
if (nextOutbound != null)
{
// If there's a next proxy, it should be the final outbound in the chain
var originalTag = outbound.tag;
outbound.tag = $"mid-{Global.ProxyTag}-{index}";
var nextOutboundCopy = JsonUtils.DeepCopy(nextOutbound);
nextOutboundCopy.tag = originalTag;
nextOutboundCopy.detour = outbound.tag; // Use detour instead of sockopt
if (prevTag != null)
{
outbound.detour = prevTag;
}
// Add to proxy tags for selector/urltest
proxyTags.Add(originalTag);
// Add in reverse order to ensure final outbound is added first
resultOutbounds.Add(nextOutboundCopy); // Final outbound (exposed to internet)
resultOutbounds.Add(outbound); // Middle outbound
}
else
{
// If no next proxy, the main outbound is the final one
if (prevTag != null)
{
outbound.detour = prevTag;
}
// Add to proxy tags for selector/urltest
proxyTags.Add(outbound.tag);
resultOutbounds.Add(outbound);
}
}
// Add urltest outbound (auto selection based on latency)
if (proxyTags.Count > 0)
{
var outUrltest = new Outbound4Sbox
{
type = "urltest",
tag = $"{Global.ProxyTag}-auto",
outbounds = proxyTags,
interrupt_exist_connections = false,
};
// Add selector outbound (manual selection)
var outSelector = new Outbound4Sbox
{
type = "selector",
tag = Global.ProxyTag,
outbounds = JsonUtils.DeepCopy(proxyTags),
interrupt_exist_connections = false,
};
outSelector.outbounds.Insert(0, outUrltest.tag);
// Insert these at the beginning
resultOutbounds.Insert(0, outUrltest);
resultOutbounds.Insert(0, outSelector);
}
// Merge results: first the selector/urltest/proxies, then other outbounds, and finally prev outbounds
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(singboxConfig.outbounds);
singboxConfig.outbounds = resultOutbounds;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenRouting(SingboxConfig singboxConfig)
{
try

View file

@ -113,7 +113,7 @@ public class CoreConfigV2rayService
await GenStatistic(v2rayConfig);
v2rayConfig.outbounds.RemoveAt(0);
var tagProxy = new List<string>();
var proxyProfiles = new List<ProfileItem>();
foreach (var it in selecteds)
{
if (it.ConfigType == EConfigType.Custom)
@ -151,17 +151,14 @@ public class CoreConfigV2rayService
}
//outbound
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(item, outbound);
outbound.tag = $"{Global.ProxyTag}-{tagProxy.Count + 1}";
v2rayConfig.outbounds.Insert(0, outbound);
tagProxy.Add(outbound.tag);
proxyProfiles.Add(item);
}
if (tagProxy.Count <= 0)
if (proxyProfiles.Count <= 0)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenOutboundsList(proxyProfiles, v2rayConfig);
//add balancers
await GenBalancer(v2rayConfig, multipleLoad);
@ -1364,6 +1361,188 @@ public class CoreConfigV2rayService
return 0;
}
private async Task<int> GenOutboundsList(List<ProfileItem> nodes, V2rayConfig v2rayConfig)
{
try
{
// Get template and initialize list
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (txtOutbound.IsNullOrEmpty())
{
return 0;
}
var resultOutbounds = new List<Outbounds4Ray>();
var prevOutbounds = new List<Outbounds4Ray>(); // Separate list for prev outbounds and fragment
// Handle fragment outbound
Outbounds4Ray? fragmentOutbound = null;
if (_config.CoreBasicItem.EnableFragment)
{
fragmentOutbound = new Outbounds4Ray
{
protocol = "freedom",
tag = $"fragment-{Global.ProxyTag}",
settings = new()
{
fragment = new()
{
packets = _config.Fragment4RayItem?.Packets,
length = _config.Fragment4RayItem?.Length,
interval = _config.Fragment4RayItem?.Interval
}
}
};
// Add to prevOutbounds instead of v2rayConfig.outbounds
prevOutbounds.Add(fragmentOutbound);
}
// Cache for chain proxies to avoid duplicate generation
var chainProxyCache = new Dictionary<string, (string?, Outbounds4Ray?)>();
var prevProxyTags = new Dictionary<string, string>(); // Map from profile name to tag
int prevIndex = 0; // Index for prev outbounds
// Process nodes
int index = 0;
foreach (var node in nodes)
{
index++;
// Skip unsupported config types
if (node.ConfigType is EConfigType.Custom or EConfigType.Hysteria2 or EConfigType.TUIC)
{
continue;
}
// Handle proxy chain
string? prevTag = null;
Outbounds4Ray? nextOutbound = null;
if (!node.Subid.IsNullOrEmpty())
{
// Check if chain proxy is already cached
if (chainProxyCache.TryGetValue(node.Subid, out var chainProxy))
{
prevTag = chainProxy.Item1;
nextOutbound = chainProxy.Item2;
}
else
{
// Generate chain proxy and cache it
var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
if (subItem != null)
{
// Process previous proxy
if (!subItem.PrevProfile.IsNullOrEmpty())
{
// Check if this previous proxy was already created
if (prevProxyTags.TryGetValue(subItem.PrevProfile, out var existingTag))
{
prevTag = existingTag;
}
else
{
var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
if (prevNode != null
&& prevNode.ConfigType != EConfigType.Custom
&& prevNode.ConfigType != EConfigType.Hysteria2
&& prevNode.ConfigType != EConfigType.TUIC)
{
prevIndex++;
var prevOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
prevTag = $"{Global.ProxyTag}-prev-{prevIndex}";
prevOutbound.tag = prevTag;
prevProxyTags[subItem.PrevProfile] = prevTag;
// Set fragment if needed
if (fragmentOutbound != null && prevOutbound.streamSettings?.security.IsNullOrEmpty() == false)
{
prevOutbound.streamSettings.sockopt = new()
{
dialerProxy = fragmentOutbound.tag
};
}
// Add to prev outbounds list (will be added at the end)
prevOutbounds.Add(prevOutbound);
}
}
}
// Process next proxy
var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode != null
&& nextNode.ConfigType != EConfigType.Custom
&& nextNode.ConfigType != EConfigType.Hysteria2
&& nextNode.ConfigType != EConfigType.TUIC)
{
nextOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(nextNode, nextOutbound);
}
// Cache the chain proxy
chainProxyCache[node.Subid] = (prevTag, nextOutbound);
}
}
}
// Create main outbound
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(node, outbound);
outbound.tag = $"{Global.ProxyTag}-{index}";
// Configure proxy chain relationships
if (nextOutbound != null)
{
// If there's a next proxy, it should be the final outbound in the chain
var originalTag = outbound.tag;
outbound.tag = $"mid-{Global.ProxyTag}-{index}";
var nextOutboundCopy = JsonUtils.DeepCopy(nextOutbound);
nextOutboundCopy.tag = originalTag;
nextOutboundCopy.streamSettings.sockopt = new() { dialerProxy = outbound.tag };
if (prevTag != null)
{
outbound.streamSettings.sockopt = new() { dialerProxy = prevTag };
}
// Add in reverse order to ensure final outbound is added first
resultOutbounds.Add(nextOutboundCopy); // Final outbound (exposed to internet)
resultOutbounds.Add(outbound); // Middle outbound
}
else
{
// If no next proxy, the main outbound is the final one
if (prevTag != null)
{
outbound.streamSettings.sockopt = new() { dialerProxy = prevTag };
}
else if (fragmentOutbound != null && outbound.streamSettings?.security.IsNullOrEmpty() == false)
{
outbound.streamSettings.sockopt = new() { dialerProxy = fragmentOutbound.tag };
}
resultOutbounds.Add(outbound);
}
}
// Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return 0;
}
private async Task<int> GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad)
{
if (multipleLoad == EMultipleLoad.LeastPing)