mirror of
https://github.com/2dust/v2rayN.git
synced 2026-11-03 19:15:04 +03:00
Add custom outbound support (#9817)
* Add custom outbound support * Add test * Add inner fmt support * Full config to outbounds * Rename to `Outbound` * AI optimized * Fix * Add bind interface placeholder --------- Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
This commit is contained in:
@@ -57,13 +57,10 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
|
||||
|
||||
ConvertGeo2Ruleset();
|
||||
|
||||
ApplyOutboundBindInterface();
|
||||
ApplyOutboundSendThrough();
|
||||
|
||||
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
|
||||
ret.Success = true;
|
||||
|
||||
ret.Data = ApplyFullConfigTemplate();
|
||||
ret.Data = ApplyFinalConfigModifiers();
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -107,7 +104,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
|
||||
|
||||
foreach (var it in selecteds)
|
||||
{
|
||||
if (!(Global.SingboxSupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType()))
|
||||
if (!(Global.SingboxSupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType() || it.ConfigType is EConfigType.Outbound))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -174,7 +171,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
|
||||
ApplyOutboundBindInterface();
|
||||
ApplyOutboundSendThrough();
|
||||
ret.Success = true;
|
||||
ret.Data = JsonUtils.Serialize(_coreConfig);
|
||||
ret.Data = ApplyCustomOutboundReplace();
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -236,7 +233,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
|
||||
|
||||
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
|
||||
ret.Success = true;
|
||||
ret.Data = JsonUtils.Serialize(_coreConfig);
|
||||
ret.Data = ApplyCustomOutboundReplace();
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -2,54 +2,137 @@ namespace ServiceLib.Services.CoreConfig;
|
||||
|
||||
public partial class CoreConfigSingboxService
|
||||
{
|
||||
private string ApplyFullConfigTemplate()
|
||||
private string ApplyFinalConfigModifiers()
|
||||
{
|
||||
ApplyOutboundBindInterface();
|
||||
ApplyOutboundSendThrough();
|
||||
|
||||
var coreConfigContent = ApplyCustomOutboundReplace();
|
||||
|
||||
return ApplyFullConfigTemplate(coreConfigContent);
|
||||
}
|
||||
|
||||
private string ApplyCustomOutboundReplace()
|
||||
{
|
||||
var coreConfigContent = JsonUtils.Serialize(_coreConfig);
|
||||
if (context.CustomOutboundMap.Count == 0)
|
||||
{
|
||||
return coreConfigContent;
|
||||
}
|
||||
var coreConfigNode = JsonNode.Parse(coreConfigContent) as JsonObject;
|
||||
var coreConfigOutboundsNode = coreConfigNode?["outbounds"] as JsonArray ?? [];
|
||||
ReplaceCustomOutbounds(_coreConfig.outbounds, coreConfigOutboundsNode);
|
||||
coreConfigNode!["outbounds"] = coreConfigOutboundsNode;
|
||||
var coreConfigEndpointsNode = coreConfigNode?["endpoints"] as JsonArray ?? [];
|
||||
ReplaceCustomOutbounds(_coreConfig.endpoints, coreConfigEndpointsNode);
|
||||
if (coreConfigEndpointsNode.Count > 0)
|
||||
{
|
||||
coreConfigNode!["endpoints"] = coreConfigEndpointsNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
coreConfigNode?.Remove("endpoints");
|
||||
}
|
||||
return JsonUtils.Serialize(coreConfigNode);
|
||||
|
||||
void ReplaceCustomOutbounds(IReadOnlyList<BaseServer4Sbox>? source, JsonArray jsonArrayOutbounds)
|
||||
{
|
||||
foreach (var outbound in source ?? [])
|
||||
{
|
||||
if (!context.CustomOutboundMap.TryGetValue(outbound, out var customOutboundIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var outboundTag = outbound.tag;
|
||||
var outboundDetour = outbound.detour ?? string.Empty;
|
||||
var outboundBindInterface = outbound.bind_interface ?? string.Empty;
|
||||
var customOutboundContent = context.CustomOutboundContent[customOutboundIndex];
|
||||
var containTagPlaceholder = customOutboundContent.Contains("{{tag}}");
|
||||
var containDetourPlaceholder = customOutboundContent.Contains("{{detour}}");
|
||||
var containBindInterfacePlaceholder = customOutboundContent.Contains("{{interface}}");
|
||||
customOutboundContent = customOutboundContent.Replace("{{tag}}", outboundTag);
|
||||
customOutboundContent = customOutboundContent.Replace("{{detour}}", outboundDetour);
|
||||
customOutboundContent = customOutboundContent.Replace("{{interface}}", outboundBindInterface);
|
||||
var customOutboundObj = JsonUtils.ParseJson(customOutboundContent) as JsonObject;
|
||||
|
||||
if (!containTagPlaceholder)
|
||||
{
|
||||
customOutboundObj?["tag"] = outboundTag;
|
||||
}
|
||||
if (!containDetourPlaceholder && !outboundDetour.IsNullOrEmpty())
|
||||
{
|
||||
customOutboundObj?["detour"] = outboundDetour;
|
||||
}
|
||||
else if (outboundDetour.IsNullOrEmpty())
|
||||
{
|
||||
customOutboundObj?.Remove("detour");
|
||||
}
|
||||
if (!containBindInterfacePlaceholder && !outboundBindInterface.IsNullOrEmpty())
|
||||
{
|
||||
customOutboundObj?["bind_interface"] = outboundBindInterface;
|
||||
}
|
||||
|
||||
var index = jsonArrayOutbounds
|
||||
.Select((node, idx) => new { node, idx })
|
||||
.FirstOrDefault(x => x.node?["tag"]?.ToString() == outboundTag)?.idx ?? -1;
|
||||
if (index != -1)
|
||||
{
|
||||
jsonArrayOutbounds[index] = customOutboundObj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string ApplyFullConfigTemplate(string coreConfigContent)
|
||||
{
|
||||
var fullConfigTemplate = context.FullConfigTemplate;
|
||||
if (fullConfigTemplate is not { Enabled: true })
|
||||
{
|
||||
return JsonUtils.Serialize(_coreConfig);
|
||||
return coreConfigContent;
|
||||
}
|
||||
|
||||
var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config;
|
||||
if (fullConfigTemplateItem.IsNullOrEmpty())
|
||||
{
|
||||
return JsonUtils.Serialize(_coreConfig);
|
||||
return coreConfigContent;
|
||||
}
|
||||
|
||||
var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem);
|
||||
if (fullConfigTemplateNode == null)
|
||||
{
|
||||
return JsonUtils.Serialize(_coreConfig);
|
||||
return coreConfigContent;
|
||||
}
|
||||
|
||||
// Process outbounds
|
||||
var customOutboundsNode = fullConfigTemplateNode["outbounds"] as JsonArray ?? [];
|
||||
foreach (var outbound in _coreConfig.outbounds)
|
||||
var coreConfigNode = JsonNode.Parse(coreConfigContent);
|
||||
var coreConfigOutboundsNode = coreConfigNode?["outbounds"] as JsonArray ?? [];
|
||||
foreach (var outbound in coreConfigOutboundsNode)
|
||||
{
|
||||
if (outbound.type.ToLower() is "direct" or "block")
|
||||
if (outbound["type"]?.ToString()?.ToLower() is "direct" or "block")
|
||||
{
|
||||
if (fullConfigTemplate.AddProxyOnly == true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (outbound.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty() && !Utils.IsPrivateNetwork(outbound.server ?? string.Empty))
|
||||
if (outbound["detour"] is null && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty() && !Utils.IsPrivateNetwork(outbound["server"]?.ToString() ?? string.Empty))
|
||||
{
|
||||
outbound.detour = fullConfigTemplate.ProxyDetour;
|
||||
outbound["detour"] = fullConfigTemplate.ProxyDetour;
|
||||
}
|
||||
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));
|
||||
}
|
||||
fullConfigTemplateNode["outbounds"] = customOutboundsNode;
|
||||
|
||||
// Process endpoints
|
||||
if (_coreConfig.endpoints is { Count: > 0 })
|
||||
if (fullConfigTemplateNode["endpoints"] is JsonArray { Count: > 0 } coreConfigEndpointsNode)
|
||||
{
|
||||
var customEndpointsNode = fullConfigTemplateNode["endpoints"] as JsonArray ?? [];
|
||||
foreach (var endpoint in _coreConfig.endpoints)
|
||||
foreach (var endpoint in coreConfigEndpointsNode)
|
||||
{
|
||||
if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
|
||||
if (endpoint["detour"] is null && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
|
||||
{
|
||||
endpoint.detour = fullConfigTemplate.ProxyDetour;
|
||||
endpoint["detour"] = fullConfigTemplate.ProxyDetour;
|
||||
}
|
||||
customEndpointsNode.Add(JsonUtils.DeepCopy(endpoint));
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ public partial class CoreConfigSingboxService
|
||||
private List<BaseServer4Sbox> BuildAllProxyOutbounds(string baseTagName = Global.ProxyTag, bool withSelector = true)
|
||||
{
|
||||
var proxyOutboundList = new List<BaseServer4Sbox>();
|
||||
if (!_node.ConfigType.IsComplexType())
|
||||
if (!_node.ConfigType.IsGroupType())
|
||||
{
|
||||
var outbound = BuildProxyOutbound(baseTagName);
|
||||
proxyOutboundList.Add(outbound);
|
||||
@@ -35,6 +35,10 @@ public partial class CoreConfigSingboxService
|
||||
{
|
||||
var outbound = BuildProxyServer();
|
||||
outbound.tag = baseTagName;
|
||||
if (_node.ConfigType == EConfigType.Outbound)
|
||||
{
|
||||
context.CustomOutboundMap[outbound] = _node.IndexId;
|
||||
}
|
||||
return outbound;
|
||||
}
|
||||
|
||||
@@ -59,6 +63,20 @@ public partial class CoreConfigSingboxService
|
||||
try
|
||||
{
|
||||
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
|
||||
if (_node.ConfigType == EConfigType.Outbound)
|
||||
{
|
||||
if (_node.GetProtocolExtra().IsSingboxEndpoint == true)
|
||||
{
|
||||
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
|
||||
return endpoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
var outbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
|
||||
return outbound;
|
||||
}
|
||||
}
|
||||
|
||||
if (_node.ConfigType == EConfigType.WireGuard)
|
||||
{
|
||||
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
|
||||
@@ -593,7 +611,7 @@ public partial class CoreConfigSingboxService
|
||||
{
|
||||
type = "selector",
|
||||
tag = baseTagName,
|
||||
outbounds = JsonUtils.DeepCopy(proxyTags),
|
||||
outbounds = [.. proxyTags],
|
||||
interrupt_exist_connections = false,
|
||||
};
|
||||
outSelector.outbounds.Insert(0, outUrltest.tag);
|
||||
@@ -721,9 +739,9 @@ public partial class CoreConfigSingboxService
|
||||
return resultOutbounds;
|
||||
}
|
||||
|
||||
private static List<BaseServer4Sbox> CloneOutbounds(List<BaseServer4Sbox> source)
|
||||
private List<BaseServer4Sbox> CloneOutbounds(List<BaseServer4Sbox> source)
|
||||
{
|
||||
if (source is null || source.Count == 0)
|
||||
if (source is not { Count: > 0 })
|
||||
{
|
||||
return [];
|
||||
}
|
||||
@@ -740,9 +758,14 @@ public partial class CoreConfigSingboxService
|
||||
{
|
||||
clone = JsonUtils.DeepCopy(endpoint);
|
||||
}
|
||||
if (clone is not null)
|
||||
if (clone is null)
|
||||
{
|
||||
result.Add(clone);
|
||||
continue;
|
||||
}
|
||||
result.Add(clone);
|
||||
if (context.CustomOutboundMap.ContainsKey(item))
|
||||
{
|
||||
context.CustomOutboundMap[clone] = context.CustomOutboundMap[item];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -574,7 +574,8 @@ public partial class CoreConfigSingboxService
|
||||
|
||||
if (node == null
|
||||
|| (!Global.SingboxSupportConfigType.Contains(node.ConfigType)
|
||||
&& !node.ConfigType.IsGroupType()))
|
||||
&& !node.ConfigType.IsGroupType()
|
||||
&& node.ConfigType is not EConfigType.Outbound))
|
||||
{
|
||||
return Global.ProxyTag;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user