diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs index b6d37abb..2f3d4356 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigSingboxService.cs @@ -954,101 +954,166 @@ public class CoreConfigSingboxService { try { - var proxyOutbounds = new List(); + // Get outbound template and initialize lists var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); - var chainProxy = new Dictionary(); - var proxyTags = new List(); + if (txtOutbound.IsNullOrEmpty()) + { + return 0; + } - var index = 0; + var resultOutbounds = new List(); + var prevOutbounds = new List(); // Separate list for prev outbounds + var proxyTags = new List(); // For selector and urltest outbounds + // Cache for chain proxies to avoid duplicate generation + var chainProxyCache = new Dictionary(); + var prevProxyTags = new Dictionary(); // 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++; - Outbound4Sbox? prevOutbound = null; - Outbound4Sbox? nextOutbound = null; - if (node.Subid.IsNotEmpty()) - { - var subItem = await AppHandler.Instance.GetSubItem(node.Subid); - if (chainProxy.ContainsKey(node.Subid)) - { - (prevOutbound, nextOutbound) = chainProxy[node.Subid]; - } - else if (subItem is not null) - { - var prevNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); - if (prevNode is not null - && prevNode.ConfigType != EConfigType.Custom) - { - prevOutbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(prevNode, prevOutbound); - } - var nextNode = await AppHandler.Instance.GetProfileItemViaRemarks(subItem.NextProfile); - if (nextNode is not null - && nextNode.ConfigType != EConfigType.Custom) - { - nextOutbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(nextNode, nextOutbound); - } - - chainProxy.TryAdd(node.Subid, (prevOutbound, nextOutbound)); - - if (prevOutbound is not null) - { - prevOutbound.tag = $"1-{Global.ProxyTag}-{index}"; - proxyOutbounds.Add(prevOutbound); - } - } - } + // Skip unsupported config types if (node.ConfigType is EConfigType.Custom) { continue; } - var outbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(node, outbound); - outbound.tag = $"{Global.ProxyTag}-{index}"; - proxyTags.Add(outbound.tag); - proxyOutbounds.Add(outbound); - if (prevOutbound is not null) + // Handle proxy chain + string? prevTag = null; + Outbound4Sbox? nextOutbound = null; + + if (node.Subid.IsNotEmpty()) { - outbound.detour = prevOutbound.tag; + // 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(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(txtOutbound); + await GenOutbound(nextNode, nextOutbound); + } + + // Cache the chain proxy + chainProxyCache[node.Subid] = (prevTag, nextOutbound); + } + } } - if (nextOutbound is not null) - { - var nextOutboundCopy = JsonUtils.DeepCopy(nextOutbound); + // Create main outbound + var outbound = JsonUtils.Deserialize(txtOutbound); - nextOutboundCopy.tag = outbound.tag; - outbound.tag = $"3-{Global.ProxyTag}-{index}"; - nextOutboundCopy.detour = outbound.tag; - proxyOutbounds.Add(nextOutboundCopy); + 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 - var outUrltest = new Outbound4Sbox + // Add urltest outbound (auto selection based on latency) + if (proxyTags.Count > 0) { - type = "urltest", - tag = $"{Global.ProxyTag}-auto", - outbounds = proxyTags, - interrupt_exist_connections = false, - }; - proxyOutbounds.Insert(0, outUrltest); + var outUrltest = new Outbound4Sbox + { + type = "urltest", + tag = $"{Global.ProxyTag}-auto", + outbounds = proxyTags, + interrupt_exist_connections = false, + }; - //add selector outbound - var outSelector = new Outbound4Sbox - { - type = "selector", - tag = Global.ProxyTag, - outbounds = JsonUtils.DeepCopy(proxyTags), - interrupt_exist_connections = false, - }; - outSelector.outbounds.Insert(0, outUrltest.tag); - proxyOutbounds.Insert(0, outSelector); + // 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); - proxyOutbounds.AddRange(singboxConfig.outbounds); - singboxConfig.outbounds = proxyOutbounds; + // 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) { diff --git a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs index e449fac2..e1d103f2 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/CoreConfigV2rayService.cs @@ -1365,17 +1365,24 @@ public class CoreConfigV2rayService { try { - var proxyOutbounds = new List(); + // Get template and initialize list var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); - var chainProxy = new Dictionary(); + if (txtOutbound.IsNullOrEmpty()) + { + return 0; + } + var resultOutbounds = new List(); + var prevOutbounds = new List(); // Separate list for prev outbounds and fragment + + // Handle fragment outbound Outbounds4Ray? fragmentOutbound = null; if (_config.CoreBasicItem.EnableFragment) { fragmentOutbound = new Outbounds4Ray { protocol = "freedom", - tag = $"4-{Global.ProxyTag}", + tag = $"fragment-{Global.ProxyTag}", settings = new() { fragment = new() @@ -1386,110 +1393,147 @@ public class CoreConfigV2rayService } } }; - - v2rayConfig.outbounds.Add(fragmentOutbound); + // Add to prevOutbounds instead of v2rayConfig.outbounds + prevOutbounds.Add(fragmentOutbound); } - var index = 0; + // Cache for chain proxies to avoid duplicate generation + var chainProxyCache = new Dictionary(); + var prevProxyTags = new Dictionary(); // Map from profile name to tag + int prevIndex = 0; // Index for prev outbounds + // Process nodes + int index = 0; foreach (var node in nodes) { index++; - Outbounds4Ray? prevOutbound = null; - Outbounds4Ray? nextOutbound = null; - if (node.Subid.IsNotEmpty()) - { - var subItem = await AppHandler.Instance.GetSubItem(node.Subid); - if (chainProxy.ContainsKey(node.Subid)) - { - (prevOutbound, nextOutbound) = chainProxy[node.Subid]; - } - else if (subItem is not 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) - { - prevOutbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(prevNode, prevOutbound); - } - 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) - { - nextOutbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(nextNode, nextOutbound); - } - - chainProxy.TryAdd(node.Subid, (prevOutbound, nextOutbound)); - - if (prevOutbound is not null) - { - prevOutbound.tag = $"1-{Global.ProxyTag}-{index}"; - proxyOutbounds.Add(prevOutbound); - - if (fragmentOutbound != null - && prevOutbound.streamSettings?.security.IsNullOrEmpty() == false) - { - prevOutbound.streamSettings.sockopt = new() - { - dialerProxy = fragmentOutbound.tag - }; - } - } - } - } - if (node.ConfigType is EConfigType.Custom - or EConfigType.Hysteria2 - or EConfigType.TUIC) + // 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(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(txtOutbound); + await GenOutbound(nextNode, nextOutbound); + } + + // Cache the chain proxy + chainProxyCache[node.Subid] = (prevTag, nextOutbound); + } + } + } + + // Create main outbound var outbound = JsonUtils.Deserialize(txtOutbound); + await GenOutbound(node, outbound); outbound.tag = $"{Global.ProxyTag}-{index}"; - proxyOutbounds.Add(outbound); - if (prevOutbound is not null) + // Configure proxy chain relationships + if (nextOutbound != null) { - outbound.streamSettings.sockopt = new() - { - dialerProxy = prevOutbound.tag - }; - } - else if (fragmentOutbound != null - && outbound.streamSettings?.security.IsNullOrEmpty() == false) - { - outbound.streamSettings.sockopt = new() - { - dialerProxy = fragmentOutbound.tag - }; - } + // 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}"; - if (nextOutbound is not null) - { var nextOutboundCopy = JsonUtils.DeepCopy(nextOutbound); + nextOutboundCopy.tag = originalTag; + nextOutboundCopy.streamSettings.sockopt = new() { dialerProxy = outbound.tag }; - nextOutboundCopy.tag = outbound.tag; - outbound.tag = $"3-{Global.ProxyTag}-{index}"; - nextOutboundCopy.streamSettings.sockopt = new() + if (prevTag != null) { - dialerProxy = outbound.tag - }; - proxyOutbounds.Add(nextOutboundCopy); + 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); } } - if (fragmentOutbound != null) - { - proxyOutbounds.Add(fragmentOutbound); - } - proxyOutbounds.AddRange(v2rayConfig.outbounds); - v2rayConfig.outbounds = proxyOutbounds; + + // 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) {