mirror of
https://github.com/2dust/v2rayN.git
synced 2026-02-28 05:03:02 +00:00
756 lines
30 KiB
C#
756 lines
30 KiB
C#
namespace ServiceLib.Services.CoreConfig;
|
|
|
|
public partial class CoreConfigSingboxService
|
|
{
|
|
private void GenOutbounds()
|
|
{
|
|
var proxyOutbounds = BuildAllProxyOutbounds();
|
|
FillRangeProxy(proxyOutbounds, _coreConfig, true);
|
|
}
|
|
|
|
private List<BaseServer4Sbox> BuildAllProxyOutbounds(string baseTagName = Global.ProxyTag, bool withSelector = true)
|
|
{
|
|
var proxyOutboundList = new List<BaseServer4Sbox>();
|
|
if (!_node.ConfigType.IsComplexType())
|
|
{
|
|
var outbound = BuildProxyOutbound(baseTagName);
|
|
proxyOutboundList.Add(outbound);
|
|
}
|
|
else
|
|
{
|
|
proxyOutboundList.AddRange(BuildGroupProxyOutbounds(baseTagName));
|
|
}
|
|
if (withSelector)
|
|
{
|
|
var proxyTags = proxyOutboundList.Where(n => n.tag.StartsWith(Global.ProxyTag)).Select(n => n.tag).ToList();
|
|
if (proxyTags.Count > 1)
|
|
{
|
|
proxyOutboundList.InsertRange(0, BuildSelectorOutbounds(proxyTags, baseTagName));
|
|
}
|
|
}
|
|
return proxyOutboundList;
|
|
}
|
|
|
|
private BaseServer4Sbox BuildProxyOutbound(string baseTagName = Global.ProxyTag)
|
|
{
|
|
var outbound = BuildProxyServer();
|
|
outbound.tag = baseTagName;
|
|
return outbound;
|
|
}
|
|
|
|
private List<BaseServer4Sbox> BuildGroupProxyOutbounds(string baseTagName = Global.ProxyTag)
|
|
{
|
|
var proxyOutboundList = new List<BaseServer4Sbox>();
|
|
switch (_node.ConfigType)
|
|
{
|
|
case EConfigType.PolicyGroup:
|
|
proxyOutboundList = BuildOutboundsList(baseTagName);
|
|
break;
|
|
|
|
case EConfigType.ProxyChain:
|
|
proxyOutboundList = BuildChainOutboundsList(baseTagName);
|
|
break;
|
|
}
|
|
return proxyOutboundList;
|
|
}
|
|
|
|
private BaseServer4Sbox BuildProxyServer()
|
|
{
|
|
try
|
|
{
|
|
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
|
|
if (_node.ConfigType == EConfigType.WireGuard)
|
|
{
|
|
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
|
|
FillEndpoint(endpoint);
|
|
return endpoint;
|
|
}
|
|
else
|
|
{
|
|
var outbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
|
|
FillOutbound(outbound);
|
|
return outbound;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.SaveLog(_tag, ex);
|
|
}
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
private void FillOutbound(Outbound4Sbox outbound)
|
|
{
|
|
try
|
|
{
|
|
var protocolExtra = _node.GetProtocolExtra();
|
|
outbound.server = _node.Address;
|
|
outbound.server_port = _node.Port;
|
|
outbound.type = Global.ProtocolTypes[_node.ConfigType];
|
|
|
|
switch (_node.ConfigType)
|
|
{
|
|
case EConfigType.VMess:
|
|
{
|
|
outbound.uuid = _node.Password;
|
|
outbound.alter_id = int.TryParse(protocolExtra.AlterId, out var result) ? result : 0;
|
|
if (Global.VmessSecurities.Contains(protocolExtra.VmessSecurity))
|
|
{
|
|
outbound.security = protocolExtra.VmessSecurity;
|
|
}
|
|
else
|
|
{
|
|
outbound.security = Global.DefaultSecurity;
|
|
}
|
|
|
|
FillOutboundMux(outbound);
|
|
FillOutboundTransport(outbound);
|
|
break;
|
|
}
|
|
case EConfigType.Shadowsocks:
|
|
{
|
|
outbound.method = AppManager.Instance.GetShadowsocksSecurities(_node).Contains(protocolExtra.SsMethod)
|
|
? protocolExtra.SsMethod : Global.None;
|
|
outbound.password = _node.Password;
|
|
outbound.udp_over_tcp = protocolExtra.Uot == true ? true : null;
|
|
|
|
if (_node.Network == nameof(ETransport.tcp) && _node.HeaderType == Global.TcpHeaderHttp)
|
|
{
|
|
outbound.plugin = "obfs-local";
|
|
outbound.plugin_opts = $"obfs=http;obfs-host={_node.RequestHost};";
|
|
}
|
|
else
|
|
{
|
|
var pluginArgs = string.Empty;
|
|
if (_node.Network == nameof(ETransport.ws))
|
|
{
|
|
pluginArgs += "mode=websocket;";
|
|
pluginArgs += $"host={_node.RequestHost};";
|
|
// https://github.com/shadowsocks/v2ray-plugin/blob/e9af1cdd2549d528deb20a4ab8d61c5fbe51f306/args.go#L172
|
|
// Equal signs and commas [and backslashes] must be escaped with a backslash.
|
|
var path = _node.Path.Replace("\\", "\\\\").Replace("=", "\\=").Replace(",", "\\,");
|
|
pluginArgs += $"path={path};";
|
|
}
|
|
else if (_node.Network == nameof(ETransport.quic))
|
|
{
|
|
pluginArgs += "mode=quic;";
|
|
}
|
|
if (_node.StreamSecurity == Global.StreamSecurity)
|
|
{
|
|
pluginArgs += "tls;";
|
|
var certs = CertPemManager.ParsePemChain(_node.Cert);
|
|
if (certs.Count > 0)
|
|
{
|
|
var cert = certs.First();
|
|
const string beginMarker = "-----BEGIN CERTIFICATE-----\n";
|
|
const string endMarker = "\n-----END CERTIFICATE-----";
|
|
|
|
var base64Content = cert.Replace(beginMarker, "").Replace(endMarker, "").Trim();
|
|
|
|
base64Content = base64Content.Replace("=", "\\=");
|
|
|
|
pluginArgs += $"certRaw={base64Content};";
|
|
}
|
|
}
|
|
if (pluginArgs.Length > 0)
|
|
{
|
|
outbound.plugin = "v2ray-plugin";
|
|
pluginArgs += "mux=0;";
|
|
// pluginStr remove last ';'
|
|
pluginArgs = pluginArgs[..^1];
|
|
outbound.plugin_opts = pluginArgs;
|
|
}
|
|
}
|
|
|
|
FillOutboundMux(outbound);
|
|
break;
|
|
}
|
|
case EConfigType.SOCKS:
|
|
{
|
|
outbound.version = "5";
|
|
if (_node.Username.IsNotEmpty()
|
|
&& _node.Password.IsNotEmpty())
|
|
{
|
|
outbound.username = _node.Username;
|
|
outbound.password = _node.Password;
|
|
}
|
|
break;
|
|
}
|
|
case EConfigType.HTTP:
|
|
{
|
|
if (_node.Username.IsNotEmpty()
|
|
&& _node.Password.IsNotEmpty())
|
|
{
|
|
outbound.username = _node.Username;
|
|
outbound.password = _node.Password;
|
|
}
|
|
break;
|
|
}
|
|
case EConfigType.VLESS:
|
|
{
|
|
outbound.uuid = _node.Password;
|
|
|
|
outbound.packet_encoding = "xudp";
|
|
|
|
if (!protocolExtra.Flow.IsNullOrEmpty())
|
|
{
|
|
outbound.flow = protocolExtra.Flow;
|
|
}
|
|
else
|
|
{
|
|
FillOutboundMux(outbound);
|
|
}
|
|
|
|
FillOutboundTransport(outbound);
|
|
break;
|
|
}
|
|
case EConfigType.Trojan:
|
|
{
|
|
outbound.password = _node.Password;
|
|
|
|
FillOutboundMux(outbound);
|
|
FillOutboundTransport(outbound);
|
|
break;
|
|
}
|
|
case EConfigType.Hysteria2:
|
|
{
|
|
outbound.password = _node.Password;
|
|
|
|
if (!protocolExtra.SalamanderPass.IsNullOrEmpty())
|
|
{
|
|
outbound.obfs = new()
|
|
{
|
|
type = "salamander",
|
|
password = protocolExtra.SalamanderPass.TrimEx(),
|
|
};
|
|
}
|
|
|
|
outbound.up_mbps = protocolExtra?.UpMbps is { } su and >= 0
|
|
? su
|
|
: _config.HysteriaItem.UpMbps;
|
|
outbound.down_mbps = protocolExtra?.DownMbps is { } sd and >= 0
|
|
? sd
|
|
: _config.HysteriaItem.DownMbps;
|
|
var ports = protocolExtra?.Ports?.IsNullOrEmpty() == false ? protocolExtra.Ports : null;
|
|
if ((!ports.IsNullOrEmpty()) && (ports.Contains(':') || ports.Contains('-') || ports.Contains(',')))
|
|
{
|
|
outbound.server_port = null;
|
|
outbound.server_ports = ports.Split(',')
|
|
.Select(p => p.Trim())
|
|
.Where(p => p.IsNotEmpty())
|
|
.Select(p =>
|
|
{
|
|
var port = p.Replace('-', ':');
|
|
return port.Contains(':') ? port : $"{port}:{port}";
|
|
})
|
|
.ToList();
|
|
outbound.hop_interval = _config.HysteriaItem.HopInterval >= 5
|
|
? $"{_config.HysteriaItem.HopInterval}s"
|
|
: $"{Global.Hysteria2DefaultHopInt}s";
|
|
if (int.TryParse(protocolExtra.HopInterval, out var hiResult))
|
|
{
|
|
outbound.hop_interval = hiResult >= 5 ? $"{hiResult}s" : outbound.hop_interval;
|
|
}
|
|
else if (protocolExtra.HopInterval?.Contains('-') ?? false)
|
|
{
|
|
// may be a range like 5-10
|
|
var parts = protocolExtra.HopInterval.Split('-');
|
|
if (parts.Length == 2 && int.TryParse(parts[0], out var hiL) &&
|
|
int.TryParse(parts[0], out var hiH))
|
|
{
|
|
var hi = (hiL + hiH) / 2;
|
|
outbound.hop_interval = hi >= 5 ? $"{hi}s" : outbound.hop_interval;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case EConfigType.TUIC:
|
|
{
|
|
outbound.uuid = _node.Username;
|
|
outbound.password = _node.Password;
|
|
outbound.congestion_control = protocolExtra.CongestionControl;
|
|
break;
|
|
}
|
|
case EConfigType.Anytls:
|
|
{
|
|
outbound.password = _node.Password;
|
|
break;
|
|
}
|
|
case EConfigType.Naive:
|
|
{
|
|
outbound.username = _node.Username;
|
|
outbound.password = _node.Password;
|
|
if (outbound.network == "quic")
|
|
{
|
|
outbound.quic = true;
|
|
outbound.quic_congestion_control = protocolExtra.CongestionControl.NullIfEmpty();
|
|
}
|
|
if (protocolExtra.InsecureConcurrency > 0)
|
|
{
|
|
outbound.insecure_concurrency = protocolExtra.InsecureConcurrency;
|
|
}
|
|
outbound.udp_over_tcp = protocolExtra.Uot == true ? true : null;
|
|
break;
|
|
}
|
|
}
|
|
|
|
FillOutboundTls(outbound);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.SaveLog(_tag, ex);
|
|
}
|
|
}
|
|
|
|
private void FillEndpoint(Endpoints4Sbox endpoint)
|
|
{
|
|
try
|
|
{
|
|
var protocolExtra = _node.GetProtocolExtra();
|
|
|
|
endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress);
|
|
endpoint.type = Global.ProtocolTypes[_node.ConfigType];
|
|
|
|
switch (_node.ConfigType)
|
|
{
|
|
case EConfigType.WireGuard:
|
|
{
|
|
var peer = new Peer4Sbox
|
|
{
|
|
public_key = protocolExtra.WgPublicKey,
|
|
pre_shared_key = protocolExtra.WgPresharedKey,
|
|
reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(int.Parse).ToList(),
|
|
address = _node.Address,
|
|
port = _node.Port,
|
|
// TODO default ["0.0.0.0/0", "::/0"]
|
|
allowed_ips = new() { "0.0.0.0/0", "::/0" },
|
|
};
|
|
endpoint.private_key = _node.Password;
|
|
endpoint.mtu = protocolExtra.WgMtu > 0 ? protocolExtra.WgMtu : Global.TunMtus.First();
|
|
endpoint.peers = [peer];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.SaveLog(_tag, ex);
|
|
}
|
|
}
|
|
|
|
private void FillOutboundMux(Outbound4Sbox outbound)
|
|
{
|
|
try
|
|
{
|
|
var muxEnabled = _node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled;
|
|
if (muxEnabled && _config.Mux4SboxItem.Protocol.IsNotEmpty())
|
|
{
|
|
var mux = new Multiplex4Sbox()
|
|
{
|
|
enabled = true,
|
|
protocol = _config.Mux4SboxItem.Protocol,
|
|
max_connections = _config.Mux4SboxItem.MaxConnections,
|
|
padding = _config.Mux4SboxItem.Padding,
|
|
};
|
|
outbound.multiplex = mux;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.SaveLog(_tag, ex);
|
|
}
|
|
}
|
|
|
|
private void FillOutboundTls(Outbound4Sbox outbound)
|
|
{
|
|
try
|
|
{
|
|
if (_node.StreamSecurity is not (Global.StreamSecurityReality or Global.StreamSecurity))
|
|
{
|
|
return;
|
|
}
|
|
if (_node.ConfigType is EConfigType.Shadowsocks or EConfigType.SOCKS or EConfigType.WireGuard)
|
|
{
|
|
return;
|
|
}
|
|
var serverName = string.Empty;
|
|
if (_node.Sni.IsNotEmpty())
|
|
{
|
|
serverName = _node.Sni;
|
|
}
|
|
else if (_node.RequestHost.IsNotEmpty())
|
|
{
|
|
serverName = Utils.String2List(_node.RequestHost)?.First();
|
|
}
|
|
var tls = new Tls4Sbox()
|
|
{
|
|
enabled = true,
|
|
record_fragment = _config.CoreBasicItem.EnableFragment ? true : null,
|
|
server_name = serverName,
|
|
insecure = Utils.ToBool(_node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : _node.AllowInsecure),
|
|
alpn = _node.GetAlpn(),
|
|
};
|
|
if (_node.Fingerprint.IsNotEmpty())
|
|
{
|
|
tls.utls = new Utls4Sbox()
|
|
{
|
|
enabled = true,
|
|
fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint
|
|
};
|
|
}
|
|
if (_node.StreamSecurity == Global.StreamSecurity)
|
|
{
|
|
var certs = CertPemManager.ParsePemChain(_node.Cert);
|
|
if (certs.Count > 0)
|
|
{
|
|
tls.certificate = certs;
|
|
tls.insecure = false;
|
|
}
|
|
}
|
|
else if (_node.StreamSecurity == Global.StreamSecurityReality)
|
|
{
|
|
tls.reality = new Reality4Sbox()
|
|
{
|
|
enabled = true,
|
|
public_key = _node.PublicKey,
|
|
short_id = _node.ShortId
|
|
};
|
|
tls.insecure = false;
|
|
}
|
|
var (ech, _) = ParseEchParam(_node.EchConfigList);
|
|
if (ech is not null)
|
|
{
|
|
tls.ech = ech;
|
|
}
|
|
outbound.tls = tls;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.SaveLog(_tag, ex);
|
|
}
|
|
}
|
|
|
|
private void FillOutboundTransport(Outbound4Sbox outbound)
|
|
{
|
|
try
|
|
{
|
|
var transport = new Transport4Sbox();
|
|
|
|
switch (_node.GetNetwork())
|
|
{
|
|
case nameof(ETransport.h2):
|
|
transport.type = nameof(ETransport.http);
|
|
transport.host = _node.RequestHost.IsNullOrEmpty() ? null : Utils.String2List(_node.RequestHost);
|
|
transport.path = _node.Path.NullIfEmpty();
|
|
break;
|
|
|
|
case nameof(ETransport.tcp): //http
|
|
if (_node.HeaderType == Global.TcpHeaderHttp)
|
|
{
|
|
transport.type = nameof(ETransport.http);
|
|
transport.host = _node.RequestHost.IsNullOrEmpty() ? null : Utils.String2List(_node.RequestHost);
|
|
transport.path = _node.Path.NullIfEmpty();
|
|
}
|
|
break;
|
|
|
|
case nameof(ETransport.ws):
|
|
transport.type = nameof(ETransport.ws);
|
|
var wsPath = _node.Path;
|
|
|
|
// Parse eh and ed parameters from path using regex
|
|
if (!wsPath.IsNullOrEmpty())
|
|
{
|
|
var edRegex = new Regex(@"[?&]ed=(\d+)");
|
|
var edMatch = edRegex.Match(wsPath);
|
|
if (edMatch.Success && int.TryParse(edMatch.Groups[1].Value, out var edValue))
|
|
{
|
|
transport.max_early_data = edValue;
|
|
transport.early_data_header_name = "Sec-WebSocket-Protocol";
|
|
|
|
wsPath = edRegex.Replace(wsPath, "");
|
|
wsPath = wsPath.Replace("?&", "?");
|
|
if (wsPath.EndsWith('?'))
|
|
{
|
|
wsPath = wsPath.TrimEnd('?');
|
|
}
|
|
}
|
|
|
|
var ehRegex = new Regex(@"[?&]eh=([^&]+)");
|
|
var ehMatch = ehRegex.Match(wsPath);
|
|
if (ehMatch.Success)
|
|
{
|
|
transport.early_data_header_name = Uri.UnescapeDataString(ehMatch.Groups[1].Value);
|
|
}
|
|
}
|
|
|
|
transport.path = wsPath.NullIfEmpty();
|
|
if (_node.RequestHost.IsNotEmpty())
|
|
{
|
|
transport.headers = new()
|
|
{
|
|
Host = _node.RequestHost
|
|
};
|
|
}
|
|
break;
|
|
|
|
case nameof(ETransport.httpupgrade):
|
|
transport.type = nameof(ETransport.httpupgrade);
|
|
transport.path = _node.Path.NullIfEmpty();
|
|
transport.host = _node.RequestHost.NullIfEmpty();
|
|
|
|
break;
|
|
|
|
case nameof(ETransport.quic):
|
|
transport.type = nameof(ETransport.quic);
|
|
break;
|
|
|
|
case nameof(ETransport.grpc):
|
|
transport.type = nameof(ETransport.grpc);
|
|
transport.service_name = _node.Path;
|
|
transport.idle_timeout = _config.GrpcItem.IdleTimeout?.ToString("##s");
|
|
transport.ping_timeout = _config.GrpcItem.HealthCheckTimeout?.ToString("##s");
|
|
transport.permit_without_stream = _config.GrpcItem.PermitWithoutStream;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
if (transport.type != null)
|
|
{
|
|
outbound.transport = transport;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.SaveLog(_tag, ex);
|
|
}
|
|
}
|
|
|
|
private List<Outbound4Sbox> BuildSelectorOutbounds(List<string> proxyTags, string baseTagName = Global.ProxyTag)
|
|
{
|
|
var multipleLoad = _node.GetProtocolExtra().MultipleLoad ?? EMultipleLoad.LeastPing;
|
|
var outUrltest = new Outbound4Sbox
|
|
{
|
|
type = "urltest",
|
|
tag = $"{baseTagName}-auto",
|
|
outbounds = proxyTags,
|
|
interrupt_exist_connections = false,
|
|
};
|
|
|
|
if (multipleLoad == EMultipleLoad.Fallback)
|
|
{
|
|
outUrltest.tolerance = 5000;
|
|
}
|
|
|
|
// Add selector outbound (manual selection)
|
|
var outSelector = new Outbound4Sbox
|
|
{
|
|
type = "selector",
|
|
tag = baseTagName,
|
|
outbounds = JsonUtils.DeepCopy(proxyTags),
|
|
interrupt_exist_connections = false,
|
|
};
|
|
outSelector.outbounds.Insert(0, outUrltest.tag);
|
|
|
|
return [outSelector, outUrltest];
|
|
}
|
|
|
|
private List<BaseServer4Sbox> BuildOutboundsList(string baseTagName = Global.ProxyTag)
|
|
{
|
|
var nodes = new List<ProfileItem>();
|
|
foreach (var nodeId in Utils.String2List(_node.GetProtocolExtra().ChildItems) ?? [])
|
|
{
|
|
if (context.AllProxiesMap.TryGetValue(nodeId, out var node))
|
|
{
|
|
nodes.Add(node);
|
|
}
|
|
}
|
|
var resultOutbounds = new List<BaseServer4Sbox>();
|
|
for (var i = 0; i < nodes.Count; i++)
|
|
{
|
|
var node = nodes[i];
|
|
var currentTag = $"{baseTagName}-{i + 1}";
|
|
|
|
if (node.ConfigType.IsGroupType())
|
|
{
|
|
var childProfiles = new CoreConfigSingboxService(context with { Node = node, }).BuildGroupProxyOutbounds(currentTag);
|
|
resultOutbounds.AddRange(childProfiles);
|
|
continue;
|
|
}
|
|
var outbound = new CoreConfigSingboxService(context with { Node = node, }).BuildProxyOutbound();
|
|
outbound.tag = currentTag;
|
|
resultOutbounds.Add(outbound);
|
|
}
|
|
return resultOutbounds;
|
|
}
|
|
|
|
private List<BaseServer4Sbox> BuildChainOutboundsList(string baseTagName = Global.ProxyTag)
|
|
{
|
|
var nodes = new List<ProfileItem>();
|
|
foreach (var nodeId in Utils.String2List(_node.GetProtocolExtra().ChildItems) ?? [])
|
|
{
|
|
if (context.AllProxiesMap.TryGetValue(nodeId, out var node))
|
|
{
|
|
nodes.Add(node);
|
|
}
|
|
}
|
|
// Based on actual network flow instead of data packets
|
|
var nodesReverse = nodes.AsEnumerable().Reverse().ToList();
|
|
var resultOutbounds = new List<BaseServer4Sbox>();
|
|
for (var i = 0; i < nodesReverse.Count; i++)
|
|
{
|
|
var node = nodesReverse[i];
|
|
var currentTag = i == 0 ? baseTagName : $"chain-{baseTagName}-{i}";
|
|
var dialerProxyTag = i != nodesReverse.Count - 1 ? $"chain-{baseTagName}-{i + 1}" : null;
|
|
if (node.ConfigType.IsGroupType())
|
|
{
|
|
var childProfiles = new CoreConfigSingboxService(context with { Node = node, }).BuildGroupProxyOutbounds(currentTag);
|
|
if (!dialerProxyTag.IsNullOrEmpty())
|
|
{
|
|
var chainEndNodes =
|
|
childProfiles.Where(n => n?.detour.IsNullOrEmpty() ?? true);
|
|
foreach (var chainEndNode in chainEndNodes)
|
|
{
|
|
chainEndNode.detour = dialerProxyTag;
|
|
}
|
|
}
|
|
if (i != 0)
|
|
{
|
|
var chainStartNodes = childProfiles.Where(n => n.tag.StartsWith(currentTag)).ToList();
|
|
if (chainStartNodes.Count == 1)
|
|
{
|
|
foreach (var existedChainEndNode in resultOutbounds.Where(n => n.detour == currentTag))
|
|
{
|
|
existedChainEndNode.detour = chainStartNodes.First().tag;
|
|
}
|
|
}
|
|
else if (chainStartNodes.Count > 1)
|
|
{
|
|
var existedChainNodes = CloneOutbounds(resultOutbounds);
|
|
resultOutbounds.Clear();
|
|
var j = 0;
|
|
foreach (var chainStartNode in chainStartNodes)
|
|
{
|
|
var existedChainNodesClone = CloneOutbounds(existedChainNodes);
|
|
foreach (var existedChainNode in existedChainNodesClone)
|
|
{
|
|
var cloneTag = $"{existedChainNode.tag}-clone-{j + 1}";
|
|
existedChainNode.tag = cloneTag;
|
|
}
|
|
for (var k = 0; k < existedChainNodesClone.Count; k++)
|
|
{
|
|
var existedChainNode = existedChainNodesClone[k];
|
|
var previousDialerProxyTag = existedChainNode.detour;
|
|
var nextTag = k + 1 < existedChainNodesClone.Count
|
|
? existedChainNodesClone[k + 1].tag
|
|
: chainStartNode.tag;
|
|
existedChainNode.detour = (previousDialerProxyTag == currentTag)
|
|
? chainStartNode.tag
|
|
: nextTag;
|
|
resultOutbounds.Add(existedChainNode);
|
|
}
|
|
j++;
|
|
}
|
|
}
|
|
}
|
|
resultOutbounds.AddRange(childProfiles);
|
|
continue;
|
|
}
|
|
var outbound = new CoreConfigSingboxService(context with { Node = node, }).BuildProxyOutbound();
|
|
|
|
outbound.tag = currentTag;
|
|
|
|
if (!dialerProxyTag.IsNullOrEmpty())
|
|
{
|
|
outbound.detour = dialerProxyTag;
|
|
}
|
|
|
|
resultOutbounds.Add(outbound);
|
|
}
|
|
return resultOutbounds;
|
|
}
|
|
|
|
private static List<BaseServer4Sbox> CloneOutbounds(List<BaseServer4Sbox> source)
|
|
{
|
|
if (source is null || source.Count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var result = new List<BaseServer4Sbox>(source.Count);
|
|
foreach (var item in source)
|
|
{
|
|
BaseServer4Sbox? clone = null;
|
|
if (item is Outbound4Sbox outbound)
|
|
{
|
|
clone = JsonUtils.DeepCopy(outbound);
|
|
}
|
|
else if (item is Endpoints4Sbox endpoint)
|
|
{
|
|
clone = JsonUtils.DeepCopy(endpoint);
|
|
}
|
|
if (clone is not null)
|
|
{
|
|
result.Add(clone);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static void FillRangeProxy(List<BaseServer4Sbox> servers, SingboxConfig singboxConfig, bool prepend = true)
|
|
{
|
|
try
|
|
{
|
|
if (servers is null || servers.Count <= 0)
|
|
{
|
|
return;
|
|
}
|
|
var outbounds = servers.Where(s => s is Outbound4Sbox).Cast<Outbound4Sbox>().ToList();
|
|
var endpoints = servers.Where(s => s is Endpoints4Sbox).Cast<Endpoints4Sbox>().ToList();
|
|
singboxConfig.endpoints ??= [];
|
|
if (prepend)
|
|
{
|
|
singboxConfig.outbounds.InsertRange(0, outbounds);
|
|
singboxConfig.endpoints.InsertRange(0, endpoints);
|
|
}
|
|
else
|
|
{
|
|
singboxConfig.outbounds.AddRange(outbounds);
|
|
singboxConfig.endpoints.AddRange(endpoints);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.SaveLog(_tag, ex);
|
|
}
|
|
}
|
|
|
|
private static (Ech4Sbox? ech, Server4Sbox? dnsServer) ParseEchParam(string? echConfig)
|
|
{
|
|
if (echConfig.IsNullOrEmpty())
|
|
{
|
|
return (null, null);
|
|
}
|
|
if (!echConfig.Contains("://"))
|
|
{
|
|
return (new Ech4Sbox()
|
|
{
|
|
enabled = true,
|
|
config = [$"-----BEGIN ECH CONFIGS-----\n" +
|
|
$"{echConfig}\n" +
|
|
$"-----END ECH CONFIGS-----"],
|
|
}, null);
|
|
}
|
|
var idx = echConfig.IndexOf('+');
|
|
// NOTE: query_server_name, since sing-box 1.13.0
|
|
//var queryServerName = idx > 0 ? echConfig[..idx] : null;
|
|
var echDnsServer = idx > 0 ? echConfig[(idx + 1)..] : echConfig;
|
|
return (new Ech4Sbox()
|
|
{
|
|
enabled = true,
|
|
query_server_name = null,
|
|
}, ParseDnsAddress(echDnsServer));
|
|
}
|
|
}
|