Optimize proxy chain handling (#7515)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run

* Optimize proxy chain handling

* Avoids duplicate proxy chain generation
This commit is contained in:
DHR60 2025-06-30 20:26:10 +08:00 committed by GitHub
parent 30c09a7b54
commit 84f93f2ae6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 183 additions and 254 deletions

View file

@ -918,29 +918,21 @@ public class CoreConfigSingboxService
//Previous proxy
var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
string? prevOutboundTag = null;
if (prevNode is not null
&& prevNode.ConfigType != EConfigType.Custom)
{
var prevOutbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
prevOutbound.tag = $"{Global.ProxyTag}2";
prevOutboundTag = $"prev-{Global.ProxyTag}";
prevOutbound.tag = prevOutboundTag;
singboxConfig.outbounds.Add(prevOutbound);
outbound.detour = prevOutbound.tag;
}
var nextOutbound = await GenChainOutbounds(subItem, outbound, prevOutboundTag);
//Next proxy
var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode is not null
&& nextNode.ConfigType != EConfigType.Custom)
if (nextOutbound is not null)
{
var nextOutbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
await GenOutbound(nextNode, nextOutbound);
nextOutbound.tag = Global.ProxyTag;
singboxConfig.outbounds.Insert(0, nextOutbound);
outbound.tag = $"{Global.ProxyTag}1";
nextOutbound.detour = outbound.tag;
}
}
catch (Exception ex)
@ -967,8 +959,8 @@ public class CoreConfigSingboxService
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
var nextProxyCache = new Dictionary<string, Outbound4Sbox?>();
var prevProxyTags = new Dictionary<string, string?>(); // Map from profile name to tag
int prevIndex = 0; // Index for prev outbounds
// Process each node
@ -977,112 +969,55 @@ public class CoreConfigSingboxService
{
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())
var currentOutbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
var nextOutbound = nextProxyCache.GetValueOrDefault(node.Subid, null);
if (nextOutbound != null)
{
// Check if chain proxy is already cached
if (chainProxyCache.TryGetValue(node.Subid, out var chainProxy))
nextOutbound = JsonUtils.DeepCopy(nextOutbound);
}
var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
// current proxy
await GenOutbound(node, currentOutbound);
currentOutbound.tag = $"{Global.ProxyTag}-{index}";
proxyTags.Add(currentOutbound.tag);
if (!node.Subid.IsNullOrEmpty())
{
if (prevProxyTags.TryGetValue(node.Subid, out var value))
{
prevTag = chainProxy.Item1;
nextOutbound = chainProxy.Item2;
prevTag = value; // maybe null
}
else
{
// Generate chain proxy and cache it
var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
if (subItem != null)
var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
if (prevNode is not null
&& prevNode.ConfigType != EConfigType.Custom)
{
// 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);
var prevOutbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}";
prevOutbound.tag = prevTag;
prevOutbounds.Add(prevOutbound);
}
prevProxyTags[node.Subid] = prevTag;
}
}
// 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)
nextOutbound = await GenChainOutbounds(subItem, currentOutbound, prevTag, nextOutbound);
if (!nextProxyCache.ContainsKey(node.Subid))
{
outbound.detour = prevTag;
nextProxyCache[node.Subid] = nextOutbound;
}
// 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 (nextOutbound is not null)
{
// 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);
resultOutbounds.Add(nextOutbound);
}
resultOutbounds.Add(currentOutbound);
}
// Add urltest outbound (auto selection based on latency)
@ -1124,6 +1059,53 @@ public class CoreConfigSingboxService
return 0;
}
/// <summary>
/// Generates a chained outbound configuration for the given subItem and outbound.
/// The outbound's tag must be set before calling this method.
/// Returns the next proxy's outbound configuration, which may be null if no next proxy exists.
/// </summary>
/// <param name="subItem">The subscription item containing proxy chain information.</param>
/// <param name="outbound">The current outbound configuration. Its tag must be set before calling this method.</param>
/// <param name="prevOutboundTag">The tag of the previous outbound in the chain, if any.</param>
/// <param name="nextOutbound">The outbound for the next proxy in the chain, if already created. If null, will be created inside.</param>
/// <returns>
/// The outbound configuration for the next proxy in the chain, or null if no next proxy exists.
/// </returns>
private async Task<Outbound4Sbox?> GenChainOutbounds(SubItem subItem, Outbound4Sbox outbound, string? prevOutboundTag, Outbound4Sbox? nextOutbound = null)
{
try
{
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
if (!prevOutboundTag.IsNullOrEmpty())
{
outbound.detour = prevOutboundTag;
}
// Next proxy
var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode is not null
&& nextNode.ConfigType != EConfigType.Custom)
{
if (nextOutbound == null)
{
nextOutbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
await GenOutbound(nextNode, nextOutbound);
}
nextOutbound.tag = outbound.tag;
outbound.tag = $"mid-{outbound.tag}";
nextOutbound.detour = outbound.tag;
}
return nextOutbound;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return null;
}
private async Task<int> GenRouting(SingboxConfig singboxConfig)
{
try

View file

@ -1318,6 +1318,7 @@ public class CoreConfigV2rayService
//Previous proxy
var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
string? prevOutboundTag = null;
if (prevNode is not null
&& prevNode.ConfigType != EConfigType.Custom
&& prevNode.ConfigType != EConfigType.Hysteria2
@ -1325,32 +1326,15 @@ public class CoreConfigV2rayService
{
var prevOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
prevOutbound.tag = $"{Global.ProxyTag}2";
prevOutboundTag = $"prev-{Global.ProxyTag}";
prevOutbound.tag = prevOutboundTag;
v2rayConfig.outbounds.Add(prevOutbound);
outbound.streamSettings.sockopt = new()
{
dialerProxy = prevOutbound.tag
};
}
var nextOutbound = await GenChainOutbounds(subItem, outbound, prevOutboundTag);
//Next proxy
var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode is not null
&& nextNode.ConfigType != EConfigType.Custom
&& nextNode.ConfigType != EConfigType.Hysteria2
&& nextNode.ConfigType != EConfigType.TUIC)
if (nextOutbound is not null)
{
var nextOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(nextNode, nextOutbound);
nextOutbound.tag = Global.ProxyTag;
v2rayConfig.outbounds.Insert(0, nextOutbound);
outbound.tag = $"{Global.ProxyTag}1";
nextOutbound.streamSettings.sockopt = new()
{
dialerProxy = outbound.tag
};
}
}
catch (Exception ex)
@ -1375,31 +1359,9 @@ public class CoreConfigV2rayService
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
var nextProxyCache = new Dictionary<string, Outbounds4Ray?>();
var prevProxyTags = new Dictionary<string, string?>(); // Map from profile name to tag
int prevIndex = 0; // Index for prev outbounds
// Process nodes
@ -1408,126 +1370,56 @@ public class CoreConfigV2rayService
{
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;
var currentOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
var nextOutbound = nextProxyCache.GetValueOrDefault(node.Subid, null);
if (nextOutbound != null)
{
nextOutbound = JsonUtils.DeepCopy(nextOutbound);
}
var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
// current proxy
await GenOutbound(node, currentOutbound);
currentOutbound.tag = $"{Global.ProxyTag}-{index}";
if (!node.Subid.IsNullOrEmpty())
{
// Check if chain proxy is already cached
if (chainProxyCache.TryGetValue(node.Subid, out var chainProxy))
if (prevProxyTags.TryGetValue(node.Subid, out var value))
{
prevTag = chainProxy.Item1;
nextOutbound = chainProxy.Item2;
prevTag = value; // maybe null
}
else
{
// Generate chain proxy and cache it
var subItem = await AppHandler.Instance.GetSubItem(node.Subid);
if (subItem != null)
var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
if (prevNode is not null
&& prevNode.ConfigType != EConfigType.Custom
&& prevNode.ConfigType != EConfigType.Hysteria2
&& prevNode.ConfigType != EConfigType.TUIC)
{
// 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);
var prevOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}";
prevOutbound.tag = prevTag;
prevOutbounds.Add(prevOutbound);
}
prevProxyTags[node.Subid] = prevTag;
}
nextOutbound = await GenChainOutbounds(subItem, currentOutbound, prevTag, nextOutbound);
if (!nextProxyCache.ContainsKey(node.Subid))
{
nextProxyCache[node.Subid] = 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 (nextOutbound is not 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);
resultOutbounds.Add(nextOutbound);
}
resultOutbounds.Add(currentOutbound);
}
// Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds
@ -1543,6 +1435,61 @@ public class CoreConfigV2rayService
return 0;
}
/// <summary>
/// Generates a chained outbound configuration for the given subItem and outbound.
/// The outbound's tag must be set before calling this method.
/// Returns the next proxy's outbound configuration, which may be null if no next proxy exists.
/// </summary>
/// <param name="subItem">The subscription item containing proxy chain information.</param>
/// <param name="outbound">The current outbound configuration. Its tag must be set before calling this method.</param>
/// <param name="prevOutboundTag">The tag of the previous outbound in the chain, if any.</param>
/// <param name="nextOutbound">The outbound for the next proxy in the chain, if already created. If null, will be created inside.</param>
/// <returns>
/// The outbound configuration for the next proxy in the chain, or null if no next proxy exists.
/// </returns>
private async Task<Outbounds4Ray?> GenChainOutbounds(SubItem subItem, Outbounds4Ray outbound, string? prevOutboundTag, Outbounds4Ray? nextOutbound = null)
{
try
{
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (!prevOutboundTag.IsNullOrEmpty())
{
outbound.streamSettings.sockopt = new()
{
dialerProxy = prevOutboundTag
};
}
// Next proxy
var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
if (nextNode is not null
&& nextNode.ConfigType != EConfigType.Custom
&& nextNode.ConfigType != EConfigType.Hysteria2
&& nextNode.ConfigType != EConfigType.TUIC)
{
if (nextOutbound == null)
{
nextOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(nextNode, nextOutbound);
}
nextOutbound.tag = outbound.tag;
outbound.tag = $"mid-{outbound.tag}";
nextOutbound.streamSettings.sockopt = new()
{
dialerProxy = outbound.tag
};
}
return nextOutbound;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return null;
}
private async Task<int> GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad)
{
if (multipleLoad == EMultipleLoad.LeastPing)