namespace ServiceLib.Services.CoreConfig; public partial class CoreConfigV2rayService { private async Task GenOutbound(ProfileItem node, Outbounds4Ray outbound) { try { var muxEnabled = node.MuxEnabled ?? _config.CoreBasicItem.MuxEnabled; switch (node.ConfigType) { case EConfigType.VMess: { VnextItem4Ray vnextItem; if (outbound.settings.vnext.Count <= 0) { vnextItem = new VnextItem4Ray(); outbound.settings.vnext.Add(vnextItem); } else { vnextItem = outbound.settings.vnext.First(); } vnextItem.address = node.Address; vnextItem.port = node.Port; UsersItem4Ray usersItem; if (vnextItem.users.Count <= 0) { usersItem = new UsersItem4Ray(); vnextItem.users.Add(usersItem); } else { usersItem = vnextItem.users.First(); } usersItem.id = node.Id; usersItem.alterId = node.AlterId; usersItem.email = Global.UserEMail; if (Global.VmessSecurities.Contains(node.Security)) { usersItem.security = node.Security; } else { usersItem.security = Global.DefaultSecurity; } await GenOutboundMux(node, outbound, muxEnabled, muxEnabled); outbound.settings.servers = null; break; } case EConfigType.Shadowsocks: { ServersItem4Ray serversItem; if (outbound.settings.servers.Count <= 0) { serversItem = new ServersItem4Ray(); outbound.settings.servers.Add(serversItem); } else { serversItem = outbound.settings.servers.First(); } serversItem.address = node.Address; serversItem.port = node.Port; serversItem.password = node.Id; serversItem.method = AppManager.Instance.GetShadowsocksSecurities(node).Contains(node.Security) ? node.Security : "none"; serversItem.ota = false; serversItem.level = 1; await GenOutboundMux(node, outbound); outbound.settings.vnext = null; break; } case EConfigType.SOCKS: case EConfigType.HTTP: { ServersItem4Ray serversItem; if (outbound.settings.servers.Count <= 0) { serversItem = new ServersItem4Ray(); outbound.settings.servers.Add(serversItem); } else { serversItem = outbound.settings.servers.First(); } serversItem.address = node.Address; serversItem.port = node.Port; serversItem.method = null; serversItem.password = null; if (node.Security.IsNotEmpty() && node.Id.IsNotEmpty()) { SocksUsersItem4Ray socksUsersItem = new() { user = node.Security, pass = node.Id, level = 1 }; serversItem.users = new List() { socksUsersItem }; } await GenOutboundMux(node, outbound); outbound.settings.vnext = null; break; } case EConfigType.VLESS: { VnextItem4Ray vnextItem; if (outbound.settings.vnext?.Count <= 0) { vnextItem = new VnextItem4Ray(); outbound.settings.vnext.Add(vnextItem); } else { vnextItem = outbound.settings.vnext.First(); } vnextItem.address = node.Address; vnextItem.port = node.Port; UsersItem4Ray usersItem; if (vnextItem.users.Count <= 0) { usersItem = new UsersItem4Ray(); vnextItem.users.Add(usersItem); } else { usersItem = vnextItem.users.First(); } usersItem.id = node.Id; usersItem.email = Global.UserEMail; usersItem.encryption = node.Security; if (node.Flow.IsNullOrEmpty()) { await GenOutboundMux(node, outbound, muxEnabled, muxEnabled); } else { usersItem.flow = node.Flow; await GenOutboundMux(node, outbound, false, muxEnabled); } outbound.settings.servers = null; break; } case EConfigType.Trojan: { ServersItem4Ray serversItem; if (outbound.settings.servers.Count <= 0) { serversItem = new ServersItem4Ray(); outbound.settings.servers.Add(serversItem); } else { serversItem = outbound.settings.servers.First(); } serversItem.address = node.Address; serversItem.port = node.Port; serversItem.password = node.Id; serversItem.ota = false; serversItem.level = 1; await GenOutboundMux(node, outbound); outbound.settings.vnext = null; break; } case EConfigType.WireGuard: { var peer = new WireguardPeer4Ray { publicKey = node.PublicKey, endpoint = node.Address + ":" + node.Port.ToString() }; var setting = new Outboundsettings4Ray { address = Utils.String2List(node.RequestHost), secretKey = node.Id, reserved = Utils.String2List(node.Path)?.Select(int.Parse).ToList(), mtu = node.ShortId.IsNullOrEmpty() ? Global.TunMtus.First() : node.ShortId.ToInt(), peers = new List { peer } }; outbound.settings = setting; outbound.settings.vnext = null; outbound.settings.servers = null; break; } } outbound.protocol = Global.ProtocolTypes[node.ConfigType]; await GenBoundStreamSettings(node, outbound); } catch (Exception ex) { Logging.SaveLog(_tag, ex); } return 0; } private async Task GenOutboundMux(ProfileItem node, Outbounds4Ray outbound, bool enabledTCP = false, bool enabledUDP = false) { try { outbound.mux.enabled = false; outbound.mux.concurrency = -1; if (enabledTCP) { outbound.mux.enabled = true; outbound.mux.concurrency = _config.Mux4RayItem.Concurrency; } else if (enabledUDP) { outbound.mux.enabled = true; outbound.mux.xudpConcurrency = _config.Mux4RayItem.XudpConcurrency; outbound.mux.xudpProxyUDP443 = _config.Mux4RayItem.XudpProxyUDP443; } } catch (Exception ex) { Logging.SaveLog(_tag, ex); } return await Task.FromResult(0); } private async Task GenBoundStreamSettings(ProfileItem node, Outbounds4Ray outbound) { try { var streamSettings = outbound.streamSettings; streamSettings.network = node.GetNetwork(); var host = node.RequestHost.TrimEx(); var path = node.Path.TrimEx(); var sni = node.Sni.TrimEx(); var useragent = ""; if (!_config.CoreBasicItem.DefUserAgent.IsNullOrEmpty()) { try { useragent = Global.UserAgentTexts[_config.CoreBasicItem.DefUserAgent]; } catch (KeyNotFoundException) { useragent = _config.CoreBasicItem.DefUserAgent; } } //if tls if (node.StreamSecurity == Global.StreamSecurity) { streamSettings.security = node.StreamSecurity; TlsSettings4Ray tlsSettings = new() { allowInsecure = Utils.ToBool(node.AllowInsecure.IsNullOrEmpty() ? _config.CoreBasicItem.DefAllowInsecure.ToString().ToLower() : node.AllowInsecure), alpn = node.GetAlpn(), fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint }; if (sni.IsNotEmpty()) { tlsSettings.serverName = sni; } else if (host.IsNotEmpty()) { tlsSettings.serverName = Utils.String2List(host)?.First(); } streamSettings.tlsSettings = tlsSettings; } //if Reality if (node.StreamSecurity == Global.StreamSecurityReality) { streamSettings.security = node.StreamSecurity; TlsSettings4Ray realitySettings = new() { fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint, serverName = sni, publicKey = node.PublicKey, shortId = node.ShortId, spiderX = node.SpiderX, mldsa65Verify = node.Mldsa65Verify, show = false, }; streamSettings.realitySettings = realitySettings; } //streamSettings switch (node.GetNetwork()) { case nameof(ETransport.kcp): KcpSettings4Ray kcpSettings = new() { mtu = _config.KcpItem.Mtu, tti = _config.KcpItem.Tti }; kcpSettings.uplinkCapacity = _config.KcpItem.UplinkCapacity; kcpSettings.downlinkCapacity = _config.KcpItem.DownlinkCapacity; kcpSettings.congestion = _config.KcpItem.Congestion; kcpSettings.readBufferSize = _config.KcpItem.ReadBufferSize; kcpSettings.writeBufferSize = _config.KcpItem.WriteBufferSize; kcpSettings.header = new Header4Ray { type = node.HeaderType, domain = host.IsNullOrEmpty() ? null : host }; if (path.IsNotEmpty()) { kcpSettings.seed = path; } streamSettings.kcpSettings = kcpSettings; break; //ws case nameof(ETransport.ws): WsSettings4Ray wsSettings = new(); wsSettings.headers = new Headers4Ray(); if (host.IsNotEmpty()) { wsSettings.host = host; wsSettings.headers.Host = host; } if (path.IsNotEmpty()) { wsSettings.path = path; } if (useragent.IsNotEmpty()) { wsSettings.headers.UserAgent = useragent; } streamSettings.wsSettings = wsSettings; break; //httpupgrade case nameof(ETransport.httpupgrade): HttpupgradeSettings4Ray httpupgradeSettings = new(); if (path.IsNotEmpty()) { httpupgradeSettings.path = path; } if (host.IsNotEmpty()) { httpupgradeSettings.host = host; } streamSettings.httpupgradeSettings = httpupgradeSettings; break; //xhttp case nameof(ETransport.xhttp): streamSettings.network = ETransport.xhttp.ToString(); XhttpSettings4Ray xhttpSettings = new(); if (path.IsNotEmpty()) { xhttpSettings.path = path; } if (host.IsNotEmpty()) { xhttpSettings.host = host; } if (node.HeaderType.IsNotEmpty() && Global.XhttpMode.Contains(node.HeaderType)) { xhttpSettings.mode = node.HeaderType; } if (node.Extra.IsNotEmpty()) { xhttpSettings.extra = JsonUtils.ParseJson(node.Extra); } streamSettings.xhttpSettings = xhttpSettings; await GenOutboundMux(node, outbound); break; //h2 case nameof(ETransport.h2): HttpSettings4Ray httpSettings = new(); if (host.IsNotEmpty()) { httpSettings.host = Utils.String2List(host); } httpSettings.path = path; streamSettings.httpSettings = httpSettings; break; //quic case nameof(ETransport.quic): QuicSettings4Ray quicsettings = new() { security = host, key = path, header = new Header4Ray { type = node.HeaderType } }; streamSettings.quicSettings = quicsettings; if (node.StreamSecurity == Global.StreamSecurity) { if (sni.IsNotEmpty()) { streamSettings.tlsSettings.serverName = sni; } else { streamSettings.tlsSettings.serverName = node.Address; } } break; case nameof(ETransport.grpc): GrpcSettings4Ray grpcSettings = new() { authority = host.IsNullOrEmpty() ? null : host, serviceName = path, multiMode = node.HeaderType == Global.GrpcMultiMode, idle_timeout = _config.GrpcItem.IdleTimeout, health_check_timeout = _config.GrpcItem.HealthCheckTimeout, permit_without_stream = _config.GrpcItem.PermitWithoutStream, initial_windows_size = _config.GrpcItem.InitialWindowsSize, }; streamSettings.grpcSettings = grpcSettings; break; default: //tcp if (node.HeaderType == Global.TcpHeaderHttp) { TcpSettings4Ray tcpSettings = new() { header = new Header4Ray { type = node.HeaderType } }; //request Host string request = EmbedUtils.GetEmbedText(Global.V2raySampleHttpRequestFileName); string[] arrHost = host.Split(','); string host2 = string.Join(",".AppendQuotes(), arrHost); request = request.Replace("$requestHost$", $"{host2.AppendQuotes()}"); request = request.Replace("$requestUserAgent$", $"{useragent.AppendQuotes()}"); //Path string pathHttp = @"/"; if (path.IsNotEmpty()) { string[] arrPath = path.Split(','); pathHttp = string.Join(",".AppendQuotes(), arrPath); } request = request.Replace("$requestPath$", $"{pathHttp.AppendQuotes()}"); tcpSettings.header.request = JsonUtils.Deserialize(request); streamSettings.tcpSettings = tcpSettings; } break; } } catch (Exception ex) { Logging.SaveLog(_tag, ex); } return 0; } private async Task GenGroupOutbound(ProfileItem node, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false) { try { if (node.ConfigType is not (EConfigType.PolicyGroup or EConfigType.ProxyChain)) { return -1; } ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); if (profileGroupItem is null || profileGroupItem.ChildItems.IsNullOrEmpty()) { return -1; } var hasCycle = await node.HasCycle(new HashSet(), new HashSet()); if (hasCycle) { return -1; } // remove custom nodes // remove group nodes for proxy chain var childProfiles = (await Task.WhenAll( Utils.String2List(profileGroupItem.ChildItems) .Where(p => !p.IsNullOrEmpty()) .Select(AppManager.Instance.GetProfileItem) )) .Where(p => p != null && p.IsValid() && p.ConfigType != EConfigType.Custom && (node.ConfigType == EConfigType.PolicyGroup || p.ConfigType < EConfigType.Group) ) .ToList(); if (childProfiles.Count <= 0) { return -1; } switch (node.ConfigType) { case EConfigType.PolicyGroup: if (ignoreOriginChain) { await GenOutboundsList(childProfiles, v2rayConfig, baseTagName); } else { await GenOutboundsListWithChain(childProfiles, v2rayConfig, baseTagName); } break; case EConfigType.ProxyChain: await GenChainOutboundsList(childProfiles, v2rayConfig, baseTagName); break; default: break; } //add balancers await GenObservatory(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName); await GenBalancer(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName); } catch (Exception ex) { Logging.SaveLog(_tag, ex); } return await Task.FromResult(0); } private async Task GenMoreOutbounds(ProfileItem node, V2rayConfig v2rayConfig) { //fragment proxy if (_config.CoreBasicItem.EnableFragment && v2rayConfig.outbounds.First().streamSettings?.security.IsNullOrEmpty() == false) { var fragmentOutbound = new Outbounds4Ray { protocol = "freedom", tag = $"{Global.ProxyTag}3", settings = new() { fragment = new() { packets = _config.Fragment4RayItem?.Packets, length = _config.Fragment4RayItem?.Length, interval = _config.Fragment4RayItem?.Interval } } }; v2rayConfig.outbounds.Add(fragmentOutbound); v2rayConfig.outbounds.First().streamSettings.sockopt = new() { dialerProxy = fragmentOutbound.tag }; return 0; } if (node.Subid.IsNullOrEmpty()) { return 0; } try { var subItem = await AppManager.Instance.GetSubItem(node.Subid); if (subItem is null) { return 0; } //current proxy var outbound = v2rayConfig.outbounds.First(); var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); //Previous proxy var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); string? prevOutboundTag = null; if (prevNode is not null && Global.XraySupportConfigType.Contains(prevNode.ConfigType)) { var prevOutbound = JsonUtils.Deserialize(txtOutbound); await GenOutbound(prevNode, prevOutbound); prevOutboundTag = $"prev-{Global.ProxyTag}"; prevOutbound.tag = prevOutboundTag; v2rayConfig.outbounds.Add(prevOutbound); } var nextOutbound = await GenChainOutbounds(subItem, outbound, prevOutboundTag); if (nextOutbound is not null) { v2rayConfig.outbounds.Insert(0, nextOutbound); } } catch (Exception ex) { Logging.SaveLog(_tag, ex); } return 0; } private async Task GenOutboundsListWithChain(List nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag) { try { // Get template and initialize list var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); if (txtOutbound.IsNullOrEmpty()) { return 0; } var resultOutbounds = new List(); var prevOutbounds = new List(); // Separate list for prev outbounds and fragment // Cache for chain proxies to avoid duplicate generation var nextProxyCache = 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++; if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) { ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty()) { continue; } var childProfiles = (await Task.WhenAll( Utils.String2List(profileGroupItem.ChildItems) .Where(p => !p.IsNullOrEmpty()) .Select(AppManager.Instance.GetProfileItem) )).Where(p => p != null).ToList(); if (childProfiles.Count <= 0) { continue; } var childBaseTagName = $"{baseTagName}-{index}"; var ret = node.ConfigType switch { EConfigType.PolicyGroup => await GenOutboundsListWithChain(childProfiles, v2rayConfig, childBaseTagName), EConfigType.ProxyChain => await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName), _ => throw new NotImplementedException() }; continue; } // Handle proxy chain string? prevTag = null; var currentOutbound = JsonUtils.Deserialize(txtOutbound); var nextOutbound = nextProxyCache.GetValueOrDefault(node.Subid, null); if (nextOutbound != null) { nextOutbound = JsonUtils.DeepCopy(nextOutbound); } var subItem = await AppManager.Instance.GetSubItem(node.Subid); // current proxy await GenOutbound(node, currentOutbound); currentOutbound.tag = $"{baseTagName}-{index}"; if (!node.Subid.IsNullOrEmpty()) { if (prevProxyTags.TryGetValue(node.Subid, out var value)) { prevTag = value; // maybe null } else { var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); if (prevNode is not null && Global.XraySupportConfigType.Contains(prevNode.ConfigType)) { var prevOutbound = JsonUtils.Deserialize(txtOutbound); await GenOutbound(prevNode, prevOutbound); prevTag = $"prev-{baseTagName}-{++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; } } if (nextOutbound is not null) { resultOutbounds.Add(nextOutbound); } resultOutbounds.Add(currentOutbound); } // 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; } /// /// 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. /// /// The subscription item containing proxy chain information. /// The current outbound configuration. Its tag must be set before calling this method. /// The tag of the previous outbound in the chain, if any. /// The outbound for the next proxy in the chain, if already created. If null, will be created inside. /// /// The outbound configuration for the next proxy in the chain, or null if no next proxy exists. /// private async Task 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 AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); if (nextNode is not null && Global.XraySupportConfigType.Contains(nextNode.ConfigType)) { if (nextOutbound == null) { nextOutbound = JsonUtils.Deserialize(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 GenOutboundsList(List nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag) { var resultOutbounds = new List(); for (var i = 0; i < nodes.Count; i++) { var node = nodes[i]; var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); if (txtOutbound.IsNullOrEmpty()) { break; } var outbound = JsonUtils.Deserialize(txtOutbound); var result = await GenOutbound(node, outbound); if (result != 0) { break; } outbound.tag = baseTagName + (i + 1).ToString(); resultOutbounds.Add(outbound); } v2rayConfig.outbounds ??= new(); resultOutbounds.AddRange(v2rayConfig.outbounds); v2rayConfig.outbounds = resultOutbounds; return await Task.FromResult(0); } private async Task GenChainOutboundsList(List nodes, V2rayConfig v2RayConfig, string baseTagName = Global.ProxyTag) { // Based on actual network flow instead of data packets var nodesReverse = nodes.AsEnumerable().Reverse().ToList(); var resultOutbounds = new List(); for (var i = 0; i < nodesReverse.Count; i++) { var node = nodesReverse[i]; var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); if (txtOutbound.IsNullOrEmpty()) { break; } var outbound = JsonUtils.Deserialize(txtOutbound); var result = await GenOutbound(node, outbound); if (result != 0) { break; } if (i == 0) { outbound.tag = baseTagName; } else { // avoid v2ray observe outbound.tag = "chain-" + baseTagName + i.ToString(); } if (i != nodesReverse.Count - 1) { outbound.streamSettings.sockopt = new() { dialerProxy = "chain-" + baseTagName + (i + 1).ToString() }; } resultOutbounds.Add(outbound); } v2RayConfig.outbounds ??= new(); resultOutbounds.AddRange(v2RayConfig.outbounds); v2RayConfig.outbounds = resultOutbounds; return await Task.FromResult(0); } }