diff --git a/v2rayN/ServiceLib/Enums/EConfigType.cs b/v2rayN/ServiceLib/Enums/EConfigType.cs index 6698f962..5ddb8451 100644 --- a/v2rayN/ServiceLib/Enums/EConfigType.cs +++ b/v2rayN/ServiceLib/Enums/EConfigType.cs @@ -12,5 +12,9 @@ public enum EConfigType TUIC = 8, WireGuard = 9, HTTP = 10, - Anytls = 11 + Anytls = 11, + + Group = 1000, + PolicyGroup = 1001, + ProxyChain = 1002, } diff --git a/v2rayN/ServiceLib/Enums/EMultipleLoad.cs b/v2rayN/ServiceLib/Enums/EMultipleLoad.cs index 42be7a5b..ef6a5ff9 100644 --- a/v2rayN/ServiceLib/Enums/EMultipleLoad.cs +++ b/v2rayN/ServiceLib/Enums/EMultipleLoad.cs @@ -2,8 +2,9 @@ namespace ServiceLib.Enums; public enum EMultipleLoad { + LeastPing, + Fallback, Random, RoundRobin, - LeastPing, LeastLoad } diff --git a/v2rayN/ServiceLib/Enums/EViewAction.cs b/v2rayN/ServiceLib/Enums/EViewAction.cs index 075439fd..fba340dc 100644 --- a/v2rayN/ServiceLib/Enums/EViewAction.cs +++ b/v2rayN/ServiceLib/Enums/EViewAction.cs @@ -23,6 +23,7 @@ public enum EViewAction RoutingRuleDetailsWindow, AddServerWindow, AddServer2Window, + AddGroupServerWindow, DNSSettingWindow, RoutingSettingWindow, OptionSettingWindow, diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index 6c4a4362..c7f18d2c 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -50,6 +50,7 @@ public class Global public const string DirectTag = "direct"; public const string BlockTag = "block"; public const string DnsTag = "dns-module"; + public const string BalancerTagSuffix = "-round"; public const string StreamSecurity = "tls"; public const string StreamSecurityReality = "reality"; public const string Loopback = "127.0.0.1"; diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index 65f2ee53..2c6428d6 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -357,6 +357,11 @@ public static class ConfigHandler { } } + else if (profileItem.ConfigType > EConfigType.Group) + { + var profileGroupItem = await AppManager.Instance.GetProfileGroupItem(it.IndexId); + await AddGroupServerCommon(config, profileItem, profileGroupItem, true); + } else { await AddServerCommon(config, profileItem, true); @@ -1074,6 +1079,37 @@ public static class ConfigHandler return 0; } + public static async Task AddGroupServerCommon(Config config, ProfileItem profileItem, ProfileGroupItem profileGroupItem, bool toFile = true) + { + var maxSort = -1; + if (profileItem.IndexId.IsNullOrEmpty()) + { + profileItem.IndexId = Utils.GetGuid(false); + maxSort = ProfileExManager.Instance.GetMaxSort(); + } + var groupType = profileItem.ConfigType == EConfigType.ProxyChain ? EConfigType.ProxyChain.ToString() : profileGroupItem.MultipleLoad.ToString(); + profileItem.Address = $"{profileItem.CoreType}-{groupType}"; + if (maxSort > 0) + { + ProfileExManager.Instance.SetSort(profileItem.IndexId, maxSort + 1); + } + if (toFile) + { + await SQLiteHelper.Instance.ReplaceAsync(profileItem); + if (profileGroupItem != null) + { + profileGroupItem.ParentIndexId = profileItem.IndexId; + await ProfileGroupItemManager.Instance.SaveItemAsync(profileGroupItem); + } + else + { + ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(profileItem.IndexId); + await ProfileGroupItemManager.Instance.SaveTo(); + } + } + return 0; + } + /// /// Compare two profile items to determine if they represent the same server /// Used for deduplication and server matching @@ -1145,7 +1181,7 @@ public static class ConfigHandler } /// - /// Create a custom server that combines multiple servers for load balancing + /// Create a group server that combines multiple servers for load balancing /// Generates a configuration file that references multiple servers /// /// Current configuration @@ -1153,45 +1189,54 @@ public static class ConfigHandler /// Core type to use (Xray or sing_box) /// Load balancing algorithm /// Result object with success state and data - public static async Task AddCustomServer4Multiple(Config config, List selecteds, ECoreType coreType, EMultipleLoad multipleLoad) + public static async Task AddGroupServer4Multiple(Config config, List selecteds, ECoreType coreType, EMultipleLoad multipleLoad, string? subId) { - var indexId = Utils.GetMd5(Global.CoreMultipleLoadConfigFileName); - var configPath = Utils.GetConfigPath(Global.CoreMultipleLoadConfigFileName); + var result = new RetResult(); - var result = await CoreConfigHandler.GenerateClientMultipleLoadConfig(config, configPath, selecteds, coreType, multipleLoad); - if (result.Success != true) - { - return result; - } + var indexId = Utils.GetGuid(false); + var childProfileIndexId = Utils.List2String(selecteds.Select(p => p.IndexId).ToList()); - if (!File.Exists(configPath)) - { - return result; - } - - var profileItem = await AppManager.Instance.GetProfileItem(indexId) ?? new(); - profileItem.IndexId = indexId; + var remark = subId.IsNullOrEmpty() ? string.Empty : $"{(await AppManager.Instance.GetSubItem(subId)).Remarks} "; if (coreType == ECoreType.Xray) { - profileItem.Remarks = multipleLoad switch + remark += multipleLoad switch { - EMultipleLoad.Random => ResUI.menuSetDefaultMultipleServerXrayRandom, - EMultipleLoad.RoundRobin => ResUI.menuSetDefaultMultipleServerXrayRoundRobin, - EMultipleLoad.LeastPing => ResUI.menuSetDefaultMultipleServerXrayLeastPing, - EMultipleLoad.LeastLoad => ResUI.menuSetDefaultMultipleServerXrayLeastLoad, - _ => ResUI.menuSetDefaultMultipleServerXrayRoundRobin, + EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerXrayLeastPing, + EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerXrayFallback, + EMultipleLoad.Random => ResUI.menuGenGroupMultipleServerXrayRandom, + EMultipleLoad.RoundRobin => ResUI.menuGenGroupMultipleServerXrayRoundRobin, + EMultipleLoad.LeastLoad => ResUI.menuGenGroupMultipleServerXrayLeastLoad, + _ => ResUI.menuGenGroupMultipleServerXrayRoundRobin, }; } else if (coreType == ECoreType.sing_box) { - profileItem.Remarks = ResUI.menuSetDefaultMultipleServerSingBoxLeastPing; + remark += multipleLoad switch + { + EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerSingBoxLeastPing, + EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerSingBoxFallback, + _ => ResUI.menuGenGroupMultipleServerSingBoxLeastPing, + }; } - profileItem.Address = Global.CoreMultipleLoadConfigFileName; - profileItem.ConfigType = EConfigType.Custom; - profileItem.CoreType = coreType; - - await AddServerCommon(config, profileItem, true); - + var profile = new ProfileItem + { + IndexId = indexId, + CoreType = coreType, + ConfigType = EConfigType.PolicyGroup, + Remarks = remark, + }; + if (!subId.IsNullOrEmpty()) + { + profile.Subid = subId; + } + var profileGroup = new ProfileGroupItem + { + ChildItems = childProfileIndexId, + MultipleLoad = multipleLoad, + ParentIndexId = indexId, + }; + var ret = await AddGroupServerCommon(config, profile, profileGroup, true); + result.Success = ret == 0; result.Data = indexId; return result; } @@ -1209,12 +1254,21 @@ public static class ConfigHandler ProfileItem? itemSocks = null; if (node.ConfigType != EConfigType.Custom && coreType != ECoreType.sing_box && config.TunModeItem.EnableTun) { + var tun2SocksAddress = node.Address; + if (node.ConfigType > EConfigType.Group) + { + var lstAddresses = (await ProfileGroupItemManager.GetAllChildDomainAddresses(node.IndexId)).ToList(); + if (lstAddresses.Count > 0) + { + tun2SocksAddress = Utils.List2String(lstAddresses); + } + } itemSocks = new ProfileItem() { CoreType = ECoreType.sing_box, ConfigType = EConfigType.SOCKS, Address = Global.Loopback, - SpiderX = node.Address, // Tun2SocksAddress + SpiderX = tun2SocksAddress, // Tun2SocksAddress Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks) }; } diff --git a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs index 66d261ac..96c72bb1 100644 --- a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs @@ -132,24 +132,4 @@ public static class CoreConfigHandler await File.WriteAllTextAsync(fileName, result.Data.ToString()); return result; } - - public static async Task GenerateClientMultipleLoadConfig(Config config, string fileName, List selecteds, ECoreType coreType, EMultipleLoad multipleLoad) - { - var result = new RetResult(); - if (coreType == ECoreType.sing_box) - { - result = await new CoreConfigSingboxService(config).GenerateClientMultipleLoadConfig(selecteds); - } - else - { - result = await new CoreConfigV2rayService(config).GenerateClientMultipleLoadConfig(selecteds, multipleLoad); - } - - if (result.Success != true) - { - return result; - } - await File.WriteAllTextAsync(fileName, result.Data.ToString()); - return result; - } } diff --git a/v2rayN/ServiceLib/Manager/AppManager.cs b/v2rayN/ServiceLib/Manager/AppManager.cs index fb2a3f39..08e96857 100644 --- a/v2rayN/ServiceLib/Manager/AppManager.cs +++ b/v2rayN/ServiceLib/Manager/AppManager.cs @@ -64,6 +64,7 @@ public sealed class AppManager SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); + SQLiteHelper.Instance.CreateTable(); return true; } @@ -207,6 +208,15 @@ public sealed class AppManager return await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(it => it.Remarks == remarks); } + public async Task GetProfileGroupItem(string parentIndexId) + { + if (parentIndexId.IsNullOrEmpty()) + { + return null; + } + return await SQLiteHelper.Instance.TableAsync().FirstOrDefaultAsync(it => it.ParentIndexId == parentIndexId); + } + public async Task?> RoutingItems() { return await SQLiteHelper.Instance.TableAsync().OrderBy(t => t.Sort).ToListAsync(); diff --git a/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs new file mode 100644 index 00000000..447e7433 --- /dev/null +++ b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs @@ -0,0 +1,276 @@ +using System.Collections.Concurrent; + +namespace ServiceLib.Manager; + +public class ProfileGroupItemManager +{ + private static readonly Lazy _instance = new(() => new()); + private ConcurrentDictionary _items = new(); + + public static ProfileGroupItemManager Instance => _instance.Value; + private static readonly string _tag = "ProfileGroupItemManager"; + + private ProfileGroupItemManager() + { + } + + public async Task Init() + { + await InitData(); + } + + // Read-only getters: do not create or mark dirty + public bool TryGet(string indexId, out ProfileGroupItem? item) + { + item = null; + if (string.IsNullOrWhiteSpace(indexId)) + { + return false; + } + + return _items.TryGetValue(indexId, out item); + } + + public ProfileGroupItem? GetOrDefault(string indexId) + { + return string.IsNullOrWhiteSpace(indexId) ? null : (_items.TryGetValue(indexId, out var v) ? v : null); + } + + private async Task InitData() + { + await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileGroupItem where parentIndexId not in ( select indexId from ProfileItem )"); + + var list = await SQLiteHelper.Instance.TableAsync().ToListAsync(); + _items = new ConcurrentDictionary(list.Where(t => !string.IsNullOrEmpty(t.ParentIndexId)).ToDictionary(t => t.ParentIndexId!)); + } + + private ProfileGroupItem AddProfileGroupItem(string indexId) + { + var profileGroupItem = new ProfileGroupItem() + { + ParentIndexId = indexId, + ChildItems = string.Empty, + MultipleLoad = EMultipleLoad.LeastPing + }; + + _items[indexId] = profileGroupItem; + return profileGroupItem; + } + + private ProfileGroupItem GetProfileGroupItem(string indexId) + { + if (string.IsNullOrEmpty(indexId)) + { + indexId = Utils.GetGuid(false); + } + + return _items.GetOrAdd(indexId, AddProfileGroupItem); + } + + public async Task ClearAll() + { + await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileGroupItem "); + _items.Clear(); + } + + public async Task SaveTo() + { + try + { + var lstExists = await SQLiteHelper.Instance.TableAsync().ToListAsync(); + var existsMap = lstExists.Where(t => !string.IsNullOrEmpty(t.ParentIndexId)).ToDictionary(t => t.ParentIndexId!); + + var lstInserts = new List(); + var lstUpdates = new List(); + + foreach (var item in _items.Values) + { + if (string.IsNullOrEmpty(item.ParentIndexId)) + { + continue; + } + + if (existsMap.ContainsKey(item.ParentIndexId)) + { + lstUpdates.Add(item); + } + else + { + lstInserts.Add(item); + } + } + + try + { + if (lstInserts.Count > 0) + { + await SQLiteHelper.Instance.InsertAllAsync(lstInserts); + } + + if (lstUpdates.Count > 0) + { + await SQLiteHelper.Instance.UpdateAllAsync(lstUpdates); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + + public ProfileGroupItem GetOrCreateAndMarkDirty(string indexId) + { + return GetProfileGroupItem(indexId); + } + + public async ValueTask DisposeAsync() + { + await SaveTo(); + } + + public async Task SaveItemAsync(ProfileGroupItem item) + { + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + + if (string.IsNullOrWhiteSpace(item.ParentIndexId)) + { + throw new ArgumentException("ParentIndexId required", nameof(item)); + } + + _items[item.ParentIndexId] = item; + + try + { + var lst = await SQLiteHelper.Instance.TableAsync().Where(t => t.ParentIndexId == item.ParentIndexId).ToListAsync(); + if (lst != null && lst.Count > 0) + { + await SQLiteHelper.Instance.UpdateAllAsync(new List { item }); + } + else + { + await SQLiteHelper.Instance.InsertAllAsync(new List { item }); + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + } + + #region Helper + + public static bool HasCycle(string? indexId) + { + return HasCycle(indexId, new HashSet(), new HashSet()); + } + + public static bool HasCycle(string? indexId, HashSet visited, HashSet stack) + { + if (indexId.IsNullOrEmpty()) + return false; + + if (stack.Contains(indexId)) + return true; + + if (visited.Contains(indexId)) + return false; + + visited.Add(indexId); + stack.Add(indexId); + + Instance.TryGet(indexId, out var groupItem); + + if (groupItem == null || groupItem.ChildItems.IsNullOrEmpty()) + { + return false; + } + + var childIds = Utils.String2List(groupItem.ChildItems) + .Where(p => !string.IsNullOrEmpty(p)) + .ToList(); + + foreach (var child in childIds) + { + if (HasCycle(child, visited, stack)) + { + return true; + } + } + + stack.Remove(indexId); + return false; + } + + public static async Task<(List Items, ProfileGroupItem? Group)> GetChildProfileItems(string? indexId) + { + Instance.TryGet(indexId, out var profileGroupItem); + if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty()) + { + return (new List(), profileGroupItem); + } + var items = await GetChildProfileItems(profileGroupItem); + return (items, profileGroupItem); + } + + public static async Task> GetChildProfileItems(ProfileGroupItem? group) + { + if (group == null || group.ChildItems.IsNullOrEmpty()) + { + return new(); + } + var childProfiles = (await Task.WhenAll( + Utils.String2List(group.ChildItems) + .Where(p => !p.IsNullOrEmpty()) + .Select(AppManager.Instance.GetProfileItem) + )) + .Where(p => + p != null && + p.IsValid() && + p.ConfigType != EConfigType.Custom + ) + .ToList(); + return childProfiles; + } + + public static async Task> GetAllChildDomainAddresses(string parentIndexId) + { + // include grand children + var childAddresses = new HashSet(); + if (!Instance.TryGet(parentIndexId, out var groupItem) || groupItem.ChildItems.IsNullOrEmpty()) + return childAddresses; + + var childIds = Utils.String2List(groupItem.ChildItems); + + foreach (var childId in childIds) + { + var childNode = await AppManager.Instance.GetProfileItem(childId); + if (childNode == null) + continue; + + if (!childNode.IsComplex()) + { + childAddresses.Add(childNode.Address); + } + else if (childNode.ConfigType > EConfigType.Group) + { + var subAddresses = await GetAllChildDomainAddresses(childNode.IndexId); + foreach (var addr in subAddresses) + { + childAddresses.Add(addr); + } + } + } + + return childAddresses; + } + + #endregion Helper +} diff --git a/v2rayN/ServiceLib/Models/ProfileGroupItem.cs b/v2rayN/ServiceLib/Models/ProfileGroupItem.cs new file mode 100644 index 00000000..6fbe1d9c --- /dev/null +++ b/v2rayN/ServiceLib/Models/ProfileGroupItem.cs @@ -0,0 +1,14 @@ +using SQLite; + +namespace ServiceLib.Models; + +[Serializable] +public class ProfileGroupItem +{ + [PrimaryKey] + public string ParentIndexId { get; set; } + + public string ChildItems { get; set; } + + public EMultipleLoad MultipleLoad { get; set; } = EMultipleLoad.LeastPing; +} diff --git a/v2rayN/ServiceLib/Models/ProfileItem.cs b/v2rayN/ServiceLib/Models/ProfileItem.cs index 998aa120..da34600b 100644 --- a/v2rayN/ServiceLib/Models/ProfileItem.cs +++ b/v2rayN/ServiceLib/Models/ProfileItem.cs @@ -32,18 +32,21 @@ public class ProfileItem : ReactiveObject public string GetSummary() { var summary = $"[{(ConfigType).ToString()}] "; - var arrAddr = Address.Contains(':') ? Address.Split(':') : Address.Split('.'); - var addr = arrAddr.Length switch + if (IsComplex()) { - > 2 => $"{arrAddr.First()}***{arrAddr.Last()}", - > 1 => $"***{arrAddr.Last()}", - _ => Address - }; - summary += ConfigType switch + summary += $"[{CoreType.ToString()}]{Remarks}"; + } + else { - EConfigType.Custom => $"[{CoreType.ToString()}]{Remarks}", - _ => $"{Remarks}({addr}:{Port})" - }; + var arrAddr = Address.Contains(':') ? Address.Split(':') : Address.Split('.'); + var addr = arrAddr.Length switch + { + > 2 => $"{arrAddr.First()}***{arrAddr.Last()}", + > 1 => $"***{arrAddr.Last()}", + _ => Address + }; + summary += $"{Remarks}({addr}:{Port})"; + } return summary; } @@ -61,6 +64,51 @@ public class ProfileItem : ReactiveObject return Network.TrimEx(); } + public bool IsComplex() + { + return ConfigType is EConfigType.Custom or > EConfigType.Group; + } + + public bool IsValid() + { + if (IsComplex()) + return true; + + if (Address.IsNullOrEmpty() || Port is <= 0 or >= 65536) + return false; + + switch (ConfigType) + { + case EConfigType.VMess: + if (Id.IsNullOrEmpty() || !Utils.IsGuidByParse(Id)) + return false; + break; + + case EConfigType.VLESS: + if (Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(Id) && Id.Length > 30)) + return false; + if (!Global.Flows.Contains(Flow)) + return false; + break; + + case EConfigType.Shadowsocks: + if (Id.IsNullOrEmpty()) + return false; + if (string.IsNullOrEmpty(Security) || !Global.SsSecuritiesInSingbox.Contains(Security)) + return false; + break; + } + + if ((ConfigType is EConfigType.VLESS or EConfigType.Trojan) + && StreamSecurity == Global.StreamSecurityReality + && PublicKey.IsNullOrEmpty()) + { + return false; + } + + return true; + } + #endregion function [PrimaryKey] diff --git a/v2rayN/ServiceLib/Models/SingboxConfig.cs b/v2rayN/ServiceLib/Models/SingboxConfig.cs index a5eec4ae..8263924a 100644 --- a/v2rayN/ServiceLib/Models/SingboxConfig.cs +++ b/v2rayN/ServiceLib/Models/SingboxConfig.cs @@ -145,6 +145,7 @@ public class Outbound4Sbox : BaseServer4Sbox public string? plugin_opts { get; set; } public List? outbounds { get; set; } public bool? interrupt_exist_connections { get; set; } + public int? tolerance { get; set; } } public class Endpoints4Sbox : BaseServer4Sbox diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index eddaa0a7..088f9b96 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -672,6 +672,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Add Child Configuration 的本地化字符串。 + /// + public static string menuAddChildServer { + get { + return ResourceManager.GetString("menuAddChildServer", resourceCulture); + } + } + /// /// 查找类似 Add a custom configuration Configuration 的本地化字符串。 /// @@ -699,6 +708,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Add Policy Group Configuration 的本地化字符串。 + /// + public static string menuAddPolicyGroupServer { + get { + return ResourceManager.GetString("menuAddPolicyGroupServer", resourceCulture); + } + } + + /// + /// 查找类似 Add Proxy Chain Configuration 的本地化字符串。 + /// + public static string menuAddProxyChainServer { + get { + return ResourceManager.GetString("menuAddProxyChainServer", resourceCulture); + } + } + /// /// 查找类似 Import Share Links from clipboard (Ctrl+V) 的本地化字符串。 /// @@ -951,6 +978,78 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Generate Policy Group from Multiple Profiles 的本地化字符串。 + /// + public static string menuGenGroupMultipleServer { + get { + return ResourceManager.GetString("menuGenGroupMultipleServer", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration Fallback by sing-box 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerSingBoxFallback { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxFallback", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration LeastPing by sing-box 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerSingBoxLeastPing { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxLeastPing", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration Fallback by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayFallback { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayFallback", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration LeastLoad by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayLeastLoad { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastLoad", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration LeastPing by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayLeastPing { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastPing", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration Random by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayRandom { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayRandom", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration RoundRobin by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayRoundRobin { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayRoundRobin", resourceCulture); + } + } + /// /// 查找类似 Global Hotkey Setting 的本地化字符串。 /// @@ -1320,6 +1419,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Remove Child Configuration 的本地化字符串。 + /// + public static string menuRemoveChildServer { + get { + return ResourceManager.GetString("menuRemoveChildServer", resourceCulture); + } + } + /// /// 查找类似 Remove duplicate Configurations 的本地化字符串。 /// @@ -1473,6 +1581,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Server List 的本地化字符串。 + /// + public static string menuServerList { + get { + return ResourceManager.GetString("menuServerList", resourceCulture); + } + } + /// /// 查找类似 Configurations 的本地化字符串。 /// @@ -1482,60 +1599,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Multi-Configuration to custom configuration 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServer { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServer", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration LeastPing by sing-box 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerSingBoxLeastPing { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerSingBoxLeastPing", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration LeastLoad by Xray 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerXrayLeastLoad { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerXrayLeastLoad", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration LeastPing by Xray 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerXrayLeastPing { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerXrayLeastPing", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration Random by Xray 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerXrayRandom { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerXrayRandom", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration RoundRobin by Xray 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerXrayRoundRobin { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerXrayRoundRobin", resourceCulture); - } - } - /// /// 查找类似 Set as active Configuration (Enter) 的本地化字符串。 /// @@ -1995,6 +2058,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Please Add At Least One Configuration 的本地化字符串。 + /// + public static string PleaseAddAtLeastOneServer { + get { + return ResourceManager.GetString("PleaseAddAtLeastOneServer", resourceCulture); + } + } + /// /// 查找类似 Please fill Remarks 的本地化字符串。 /// @@ -2373,6 +2445,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Policy Group 的本地化字符串。 + /// + public static string TbConfigTypePolicyGroup { + get { + return ResourceManager.GetString("TbConfigTypePolicyGroup", resourceCulture); + } + } + + /// + /// 查找类似 Proxy Chain 的本地化字符串。 + /// + public static string TbConfigTypeProxyChain { + get { + return ResourceManager.GetString("TbConfigTypeProxyChain", resourceCulture); + } + } + /// /// 查找类似 Confirm 的本地化字符串。 /// @@ -2526,6 +2616,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Fallback 的本地化字符串。 + /// + public static string TbFallback { + get { + return ResourceManager.GetString("TbFallback", resourceCulture); + } + } + /// /// 查找类似 Fingerprint 的本地化字符串。 /// @@ -2643,6 +2742,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Most Stable 的本地化字符串。 + /// + public static string TbLeastLoad { + get { + return ResourceManager.GetString("TbLeastLoad", resourceCulture); + } + } + + /// + /// 查找类似 Lowest Latency 的本地化字符串。 + /// + public static string TbLeastPing { + get { + return ResourceManager.GetString("TbLeastPing", resourceCulture); + } + } + /// /// 查找类似 Address (IPv4, IPv6) 的本地化字符串。 /// @@ -2697,6 +2814,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Policy Group Type 的本地化字符串。 + /// + public static string TbPolicyGroupType { + get { + return ResourceManager.GetString("TbPolicyGroupType", resourceCulture); + } + } + /// /// 查找类似 Port 的本地化字符串。 /// @@ -2769,6 +2895,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Random 的本地化字符串。 + /// + public static string TbRandom { + get { + return ResourceManager.GetString("TbRandom", resourceCulture); + } + } + /// /// 查找类似 v2ray Full Config Template 的本地化字符串。 /// @@ -2832,6 +2967,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Round Robin 的本地化字符串。 + /// + public static string TbRoundRobin { + get { + return ResourceManager.GetString("TbRoundRobin", resourceCulture); + } + } + /// /// 查找类似 socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 14b1ff6f..280231d8 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1377,22 +1377,22 @@ مخفی و پورت می شود، با کاما (،) جدا می شود - - چند سرور به پیکربندی سفارشی + + Generate Policy Group from Multiple Profiles - + چند سرور تصادفی توسط Xray - + چند سرور RoundRobin توسط Xray - + چند سرور LeastPing توسط Xray - + چند سرور LeastLoad توسط Xray - + LeastPing چند سرور توسط sing-box @@ -1512,4 +1512,52 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Server List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index fb8c6a11..09796e2d 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1377,22 +1377,22 @@ A portot lefedi, vesszővel (,) elválasztva - - Több konfiguráció egyéni konfigurációra + + Generate Policy Group from Multiple Profiles - + Több konfiguráció véletlenszerűen Xray szerint - + Több konfiguráció RoundRobin Xray szerint - + Több konfiguráció legkisebb pinggel Xray szerint - + Több konfiguráció legkisebb terheléssel Xray szerint - + Több konfiguráció legkisebb pinggel sing-box szerint @@ -1512,4 +1512,52 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Server List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 5b145da3..bf9f40a5 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1377,22 +1377,22 @@ Will cover the port, separate with commas (,) - - Multi-Configuration to custom configuration + + Generate Policy Group from Multiple Profiles - + Multi-Configuration Random by Xray - + Multi-Configuration RoundRobin by Xray - + Multi-Configuration LeastPing by Xray - + Multi-Configuration LeastLoad by Xray - + Multi-Configuration LeastPing by sing-box @@ -1512,4 +1512,52 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Server List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 27c2d0a3..cde36277 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1377,22 +1377,22 @@ Заменит указанный порт, перечисляйте через запятую (,) - - От мультиконфигурации к пользовательской конфигурации + + Generate Policy Group from Multiple Profiles - + Случайный (Xray) - + Круговой (Xray) - + Минимальное RTT (минимальное время туда-обратно) (Xray) - + Минимальная нагрузка (Xray) - + Минимальное RTT (минимальное время туда-обратно) (sing-box) @@ -1512,4 +1512,52 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Server List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index a269c0a5..1506de01 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1374,22 +1374,22 @@ 会覆盖端口,多组时用逗号 (,) 隔开 - - 多配置文件产生自定义配置 (多选) + + 多配置文件生成策略组 - + 多配置文件随机 Xray - + 多配置文件负载均衡 Xray - + 多配置文件最低延迟 Xray - + 多配置文件最稳定 Xray - + 多配置文件最低延迟 sing-box @@ -1509,4 +1509,52 @@ 默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效 + + 请至少添加一个配置文件 + + + 策略组 + + + 链式代理 + + + 最低延迟 + + + 随机 + + + 负载均衡 + + + 最稳定 + + + 策略组类型 + + + 添加策略组配置文件 + + + 添加链式代理配置文件 + + + 添加子配置文件 + + + 删除子配置文件 + + + 服务器列表 + + + 故障转移 + + + 多配置文件故障转移 sing-box + + + 多配置文件故障转移 Xray + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 81a01cfc..3101cb2f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1374,22 +1374,22 @@ 會覆蓋埠,多組時用逗號 (,) 隔開 - - 多設定檔產生自訂配置 (多選) + + Generate Policy Group from Multiple Profiles - + 多設定檔隨機 Xray - + 多設定檔負載平衡 Xray - + 多設定檔最低延遲 Xray - + 多設定檔最穩定 Xray - + 多設定檔最低延遲 sing-box @@ -1509,4 +1509,52 @@ Applies globally by default, with built-in FakeIP filtering (sing-box only). + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Server List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs index 4e24d8e2..fd77480a 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs @@ -16,7 +16,7 @@ public partial class CoreConfigSingboxService(Config config) try { if (node == null - || node.Port <= 0) + || !node.IsValid()) { ret.Msg = ResUI.CheckServerSettings; return ret; @@ -28,6 +28,17 @@ public partial class CoreConfigSingboxService(Config config) } ret.Msg = ResUI.InitialConfiguration; + + if (node?.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + switch (node.ConfigType) + { + case EConfigType.PolicyGroup: + return await GenerateClientMultipleLoadConfig(node); + case EConfigType.ProxyChain: + return await GenerateClientChainConfig(node); + } + } var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient); if (result.IsNullOrEmpty()) @@ -142,12 +153,9 @@ public partial class CoreConfigSingboxService(Config config) continue; } var item = await AppManager.Instance.GetProfileItem(it.IndexId); - if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) + if (item is null || item.IsComplex() || !item.IsValid()) { - if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) - { - continue; - } + continue; } //find unused port @@ -187,27 +195,6 @@ public partial class CoreConfigSingboxService(Config config) singboxConfig.inbounds.Add(inbound); //outbound - if (item is null) - { - continue; - } - if (item.ConfigType == EConfigType.Shadowsocks - && !Global.SsSecuritiesInSingbox.Contains(item.Security)) - { - continue; - } - if (item.ConfigType == EConfigType.VLESS - && !Global.Flows.Contains(item.Flow)) - { - continue; - } - if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan - && item.StreamSecurity == Global.StreamSecurityReality - && item.PublicKey.IsNullOrEmpty()) - { - continue; - } - var server = await GenServer(item); if (server is null) { @@ -266,7 +253,8 @@ public partial class CoreConfigSingboxService(Config config) var ret = new RetResult(); try { - if (node is not { Port: > 0 }) + if (node == null + || !node.IsValid()) { ret.Msg = ResUI.CheckServerSettings; return ret; @@ -344,7 +332,7 @@ public partial class CoreConfigSingboxService(Config config) } } - public async Task GenerateClientMultipleLoadConfig(List selecteds) + public async Task GenerateClientMultipleLoadConfig(ProfileItem parentNode) { var ret = new RetResult(); try @@ -371,56 +359,77 @@ public partial class CoreConfigSingboxService(Config config) ret.Msg = ResUI.FailedGenDefaultConfiguration; return ret; } + singboxConfig.outbounds.RemoveAt(0); await GenLog(singboxConfig); await GenInbounds(singboxConfig); - await GenRouting(singboxConfig); - await GenExperimental(singboxConfig); - singboxConfig.outbounds.RemoveAt(0); - var proxyProfiles = new List(); - foreach (var it in selecteds) - { - if (!Global.SingboxSupportConfigType.Contains(it.ConfigType)) - { - continue; - } - if (it.Port <= 0) - { - continue; - } - var item = await AppManager.Instance.GetProfileItem(it.IndexId); - if (item is null) - { - continue; - } - if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) - { - if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) - { - continue; - } - } - if (item.ConfigType == EConfigType.Shadowsocks - && !Global.SsSecuritiesInSingbox.Contains(item.Security)) - { - continue; - } - if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow)) - { - continue; - } - - //outbound - proxyProfiles.Add(item); - } - if (proxyProfiles.Count <= 0) + var groupRet = await GenGroupOutbound(parentNode, singboxConfig); + if (groupRet != 0) { ret.Msg = ResUI.FailedGenDefaultConfiguration; return ret; } - await GenOutboundsList(proxyProfiles, singboxConfig); + await GenRouting(singboxConfig); + await GenExperimental(singboxConfig); + await GenDns(null, singboxConfig); + await ConvertGeo2Ruleset(singboxConfig); + + ret.Success = true; + + ret.Data = await ApplyFullConfigTemplate(singboxConfig); + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + + public async Task GenerateClientChainConfig(ProfileItem parentNode) + { + var ret = new RetResult(); + try + { + if (_config == null) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + + ret.Msg = ResUI.InitialConfiguration; + + var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient); + var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); + if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty()) + { + ret.Msg = ResUI.FailedGetDefaultConfiguration; + return ret; + } + + var singboxConfig = JsonUtils.Deserialize(result); + if (singboxConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + singboxConfig.outbounds.RemoveAt(0); + + await GenLog(singboxConfig); + await GenInbounds(singboxConfig); + + var groupRet = await GenGroupOutbound(parentNode, singboxConfig); + if (groupRet != 0) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + await GenRouting(singboxConfig); + await GenExperimental(singboxConfig); await GenDns(null, singboxConfig); await ConvertGeo2Ruleset(singboxConfig); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs index 9bd2502d..10c82eed 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -414,16 +414,19 @@ public partial class CoreConfigSingboxService return 0; } - var domain = string.Empty; + List domain = new(); if (Utils.IsDomain(node.Address)) // normal outbound { - domain = node.Address; + domain.Add(node.Address); } - else if (node.Address == Global.Loopback && node.SpiderX.IsNotEmpty() && Utils.IsDomain(node.SpiderX)) // Tun2SocksAddress + if (node.Address == Global.Loopback && node.SpiderX.IsNotEmpty()) // Tun2SocksAddress { - domain = node.SpiderX; + domain.AddRange(Utils.String2List(node.SpiderX) + .Where(Utils.IsDomain) + .Distinct() + .ToList()); } - if (domain.IsNullOrEmpty()) + if (domain.Count == 0) { return 0; } @@ -432,7 +435,7 @@ public partial class CoreConfigSingboxService singboxConfig.dns.rules.Insert(0, new Rule4Sbox { server = server, - domain = [domain], + domain = domain, }); return await Task.FromResult(0); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs index 3f03b93c..46c90b23 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs @@ -179,13 +179,21 @@ public partial class CoreConfigSingboxService if (node.ConfigType == EConfigType.WireGuard) { var endpoint = JsonUtils.Deserialize(txtOutbound); - await GenEndpoint(node, endpoint); + var ret = await GenEndpoint(node, endpoint); + if (ret != 0) + { + return null; + } return endpoint; } else { var outbound = JsonUtils.Deserialize(txtOutbound); - await GenOutbound(node, outbound); + var ret = await GenOutbound(node, outbound); + if (ret != 0) + { + return null; + } return outbound; } } @@ -196,6 +204,52 @@ public partial class CoreConfigSingboxService return await Task.FromResult(null); } + private async Task GenGroupOutbound(ProfileItem node, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false) + { + try + { + if (node.ConfigType is not (EConfigType.PolicyGroup or EConfigType.ProxyChain)) + { + return -1; + } + var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId); + if (hasCycle) + { + return -1; + } + + var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + if (childProfiles.Count <= 0) + { + return -1; + } + switch (node.ConfigType) + { + case EConfigType.PolicyGroup: + if (ignoreOriginChain) + { + await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName); + } + else + { + await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName); + } + + break; + case EConfigType.ProxyChain: + await GenChainOutboundsList(childProfiles, singboxConfig, baseTagName); + break; + default: + break; + } + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + } + return await Task.FromResult(0); + } + private async Task GenOutboundMux(ProfileItem node, Outbound4Sbox outbound) { try @@ -410,7 +464,7 @@ public partial class CoreConfigSingboxService return 0; } - private async Task GenOutboundsList(List nodes, SingboxConfig singboxConfig) + private async Task GenOutboundsListWithChain(List nodes, SingboxConfig singboxConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag) { try { @@ -438,6 +492,29 @@ public partial class CoreConfigSingboxService { index++; + if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + if (childProfiles.Count <= 0) + { + continue; + } + var childBaseTagName = $"{baseTagName}-{index}"; + var ret = node.ConfigType switch + { + EConfigType.PolicyGroup => + await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName), + EConfigType.ProxyChain => + await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName), + _ => throw new NotImplementedException() + }; + if (ret == 0) + { + proxyTags.Add(childBaseTagName); + } + continue; + } + // Handle proxy chain string? prevTag = null; var currentServer = await GenServer(node); @@ -450,7 +527,7 @@ public partial class CoreConfigSingboxService var subItem = await AppManager.Instance.GetSubItem(node.Subid); // current proxy - currentServer.tag = $"{Global.ProxyTag}-{index}"; + currentServer.tag = $"{baseTagName}-{index}"; proxyTags.Add(currentServer.tag); if (!node.Subid.IsNullOrEmpty()) @@ -467,7 +544,7 @@ public partial class CoreConfigSingboxService { var prevOutbound = JsonUtils.Deserialize(txtOutbound); await GenOutbound(prevNode, prevOutbound); - prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}"; + prevTag = $"prev-{baseTagName}-{++prevIndex}"; prevOutbound.tag = prevTag; prevOutbounds.Add(prevOutbound); } @@ -508,16 +585,21 @@ public partial class CoreConfigSingboxService var outUrltest = new Outbound4Sbox { type = "urltest", - tag = $"{Global.ProxyTag}-auto", + 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 = Global.ProxyTag, + tag = baseTagName, outbounds = JsonUtils.DeepCopy(proxyTags), interrupt_exist_connections = false, }; @@ -529,12 +611,12 @@ public partial class CoreConfigSingboxService } // Merge results: first the selector/urltest/proxies, then other outbounds, and finally prev outbounds - resultOutbounds.AddRange(prevOutbounds); - resultOutbounds.AddRange(singboxConfig.outbounds); - singboxConfig.outbounds = resultOutbounds; - singboxConfig.endpoints ??= new List(); - resultEndpoints.AddRange(singboxConfig.endpoints); - singboxConfig.endpoints = resultEndpoints; + var serverList = new List(); + serverList = serverList.Concat(prevOutbounds) + .Concat(resultOutbounds) + .Concat(resultEndpoints) + .ToList(); + await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag); } catch (Exception ex) { @@ -574,4 +656,163 @@ public partial class CoreConfigSingboxService } return null; } + + private async Task GenOutboundsList(List nodes, SingboxConfig singboxConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag) + { + var resultOutbounds = new List(); + var resultEndpoints = new List(); // For endpoints + var proxyTags = new List(); // For selector and urltest outbounds + for (var i = 0; i < nodes.Count; i++) + { + var node = nodes[i]; + if (node == null) + continue; + if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + if (childProfiles.Count <= 0) + { + continue; + } + var childBaseTagName = $"{baseTagName}-{i + 1}"; + var ret = node.ConfigType switch + { + EConfigType.PolicyGroup => + await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName), + EConfigType.ProxyChain => + await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName), + _ => throw new NotImplementedException() + }; + if (ret == 0) + { + proxyTags.Add(childBaseTagName); + } + continue; + } + var server = await GenServer(node); + if (server is null) + { + break; + } + server.tag = baseTagName + (i + 1).ToString(); + if (server is Endpoints4Sbox endpoint) + { + resultEndpoints.Add(endpoint); + } + else if (server is Outbound4Sbox outbound) + { + resultOutbounds.Add(outbound); + } + proxyTags.Add(server.tag); + } + // Add urltest outbound (auto selection based on latency) + if (proxyTags.Count > 0) + { + 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); + // Insert these at the beginning + resultOutbounds.Insert(0, outUrltest); + resultOutbounds.Insert(0, outSelector); + } + var serverList = new List(); + serverList = serverList.Concat(resultOutbounds) + .Concat(resultEndpoints) + .ToList(); + await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag); + return await Task.FromResult(0); + } + + private async Task GenChainOutboundsList(List nodes, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag) + { + // Based on actual network flow instead of data packets + var nodesReverse = nodes.AsEnumerable().Reverse().ToList(); + var resultOutbounds = new List(); + var resultEndpoints = new List(); // For endpoints + for (var i = 0; i < nodesReverse.Count; i++) + { + var node = nodesReverse[i]; + var server = await GenServer(node); + + if (server is null) + { + break; + } + + if (i == 0) + { + server.tag = baseTagName; + } + else + { + server.tag = baseTagName + i.ToString(); + } + + if (i != nodesReverse.Count - 1) + { + server.detour = baseTagName + (i + 1).ToString(); + } + + if (server is Endpoints4Sbox endpoint) + { + resultEndpoints.Add(endpoint); + } + else if (server is Outbound4Sbox outbound) + { + resultOutbounds.Add(outbound); + } + } + var serverList = new List(); + serverList = serverList.Concat(resultOutbounds) + .Concat(resultEndpoints) + .ToList(); + await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag); + return await Task.FromResult(0); + } + + private async Task AddRangeOutbounds(List servers, SingboxConfig singboxConfig, bool prepend = true) + { + try + { + if (servers is null || servers.Count <= 0) + { + return 0; + } + var outbounds = servers.Where(s => s is Outbound4Sbox).Cast().ToList(); + var endpoints = servers.Where(s => s is Endpoints4Sbox).Cast().ToList(); + singboxConfig.endpoints ??= new(); + 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); + } + return await Task.FromResult(0); + } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index 21dfb101..3994e21b 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -368,26 +368,38 @@ public partial class CoreConfigSingboxService } var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); + if (node == null - || !Global.SingboxSupportConfigType.Contains(node.ConfigType)) + || (!Global.SingboxSupportConfigType.Contains(node.ConfigType) + && node.ConfigType is not (EConfigType.PolicyGroup or EConfigType.ProxyChain))) { return Global.ProxyTag; } - var tag = Global.ProxyTag + node.IndexId.ToString(); + var tag = $"{node.IndexId}-{Global.ProxyTag}"; if (singboxConfig.outbounds.Any(o => o.tag == tag) || (singboxConfig.endpoints != null && singboxConfig.endpoints.Any(e => e.tag == tag))) { return tag; } + if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + var ret = await GenGroupOutbound(node, singboxConfig, tag); + if (ret == 0) + { + return tag; + } + return Global.ProxyTag; + } + var server = await GenServer(node); if (server is null) { return Global.ProxyTag; } - server.tag = Global.ProxyTag + node.IndexId.ToString(); + server.tag = tag; if (server is Endpoints4Sbox endpoint) { singboxConfig.endpoints ??= new(); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs index 24b41109..37c55bca 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs @@ -16,7 +16,7 @@ public partial class CoreConfigV2rayService(Config config) try { if (node == null - || node.Port <= 0) + || !node.IsValid()) { ret.Msg = ResUI.CheckServerSettings; return ret; @@ -30,6 +30,17 @@ public partial class CoreConfigV2rayService(Config config) ret.Msg = ResUI.InitialConfiguration; + if (node?.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + switch (node.ConfigType) + { + case EConfigType.PolicyGroup: + return await GenerateClientMultipleLoadConfig(node); + case EConfigType.ProxyChain: + return await GenerateClientChainConfig(node); + } + } + var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); if (result.IsNullOrEmpty()) { @@ -71,7 +82,7 @@ public partial class CoreConfigV2rayService(Config config) } } - public async Task GenerateClientMultipleLoadConfig(List selecteds, EMultipleLoad multipleLoad) + public async Task GenerateClientMultipleLoadConfig(ProfileItem parentNode) { var ret = new RetResult(); @@ -99,70 +110,50 @@ public partial class CoreConfigV2rayService(Config config) ret.Msg = ResUI.FailedGenDefaultConfiguration; return ret; } + v2rayConfig.outbounds.RemoveAt(0); await GenLog(v2rayConfig); await GenInbounds(v2rayConfig); - await GenRouting(v2rayConfig); - await GenDns(null, v2rayConfig); - await GenStatistic(v2rayConfig); - v2rayConfig.outbounds.RemoveAt(0); - var proxyProfiles = new List(); - foreach (var it in selecteds) - { - if (!Global.XraySupportConfigType.Contains(it.ConfigType)) - { - continue; - } - if (it.Port <= 0) - { - continue; - } - var item = await AppManager.Instance.GetProfileItem(it.IndexId); - if (item is null) - { - continue; - } - if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) - { - if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) - { - continue; - } - } - if (item.ConfigType == EConfigType.Shadowsocks - && !Global.SsSecuritiesInSingbox.Contains(item.Security)) - { - continue; - } - if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow)) - { - continue; - } - - //outbound - proxyProfiles.Add(item); - } - if (proxyProfiles.Count <= 0) + var groupRet = await GenGroupOutbound(parentNode, v2rayConfig); + if (groupRet != 0) { ret.Msg = ResUI.FailedGenDefaultConfiguration; return ret; } - await GenOutboundsList(proxyProfiles, v2rayConfig); - //add balancers - await GenBalancer(v2rayConfig, multipleLoad); + await GenRouting(v2rayConfig); + await GenDns(null, v2rayConfig); + await GenStatistic(v2rayConfig); - var balancer = v2rayConfig.routing.balancers.First(); + var defaultBalancerTag = $"{Global.ProxyTag}{Global.BalancerTagSuffix}"; //add rule - var rules = v2rayConfig.routing.rules.Where(t => t.outboundTag == Global.ProxyTag).ToList(); - if (rules?.Count > 0) + var rules = v2rayConfig.routing.rules; + if (rules?.Count > 0 && ((v2rayConfig.routing.balancers?.Count ?? 0) > 0)) { + var balancerTagSet = v2rayConfig.routing.balancers + .Select(b => b.tag) + .ToHashSet(); + foreach (var rule in rules) { - rule.outboundTag = null; - rule.balancerTag = balancer.tag; + if (rule.outboundTag == null) + continue; + + if (balancerTagSet.Contains(rule.outboundTag)) + { + rule.balancerTag = rule.outboundTag; + rule.outboundTag = null; + continue; + } + + var outboundWithSuffix = rule.outboundTag + Global.BalancerTagSuffix; + if (balancerTagSet.Contains(outboundWithSuffix)) + { + rule.balancerTag = outboundWithSuffix; + rule.outboundTag = null; + } } } if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch) @@ -170,7 +161,7 @@ public partial class CoreConfigV2rayService(Config config) v2rayConfig.routing.rules.Add(new() { ip = ["0.0.0.0/0", "::/0"], - balancerTag = balancer.tag, + balancerTag = defaultBalancerTag, type = "field" }); } @@ -179,7 +170,7 @@ public partial class CoreConfigV2rayService(Config config) v2rayConfig.routing.rules.Add(new() { network = "tcp,udp", - balancerTag = balancer.tag, + balancerTag = defaultBalancerTag, type = "field" }); } @@ -197,6 +188,63 @@ public partial class CoreConfigV2rayService(Config config) } } + public async Task GenerateClientChainConfig(ProfileItem parentNode) + { + var ret = new RetResult(); + + try + { + if (_config == null) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + + ret.Msg = ResUI.InitialConfiguration; + + string result = EmbedUtils.GetEmbedText(Global.V2raySampleClient); + string txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); + if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty()) + { + ret.Msg = ResUI.FailedGetDefaultConfiguration; + return ret; + } + + var v2rayConfig = JsonUtils.Deserialize(result); + if (v2rayConfig == null) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + v2rayConfig.outbounds.RemoveAt(0); + + await GenLog(v2rayConfig); + await GenInbounds(v2rayConfig); + + var groupRet = await GenGroupOutbound(parentNode, v2rayConfig); + if (groupRet != 0) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + await GenRouting(v2rayConfig); + await GenDns(null, v2rayConfig); + await GenStatistic(v2rayConfig); + + ret.Success = true; + + ret.Data = await ApplyFullConfigTemplate(v2rayConfig); + return ret; + } + catch (Exception ex) + { + Logging.SaveLog(_tag, ex); + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + } + public async Task GenerateClientSpeedtestConfig(List selecteds) { var ret = new RetResult(); @@ -255,12 +303,9 @@ public partial class CoreConfigV2rayService(Config config) continue; } var item = await AppManager.Instance.GetProfileItem(it.IndexId); - if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS) + if (item is null || item.IsComplex() || !item.IsValid()) { - if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) - { - continue; - } + continue; } //find unused port @@ -289,28 +334,6 @@ public partial class CoreConfigV2rayService(Config config) it.Port = port; it.AllowTest = true; - //outbound - if (item is null) - { - continue; - } - if (item.ConfigType == EConfigType.Shadowsocks - && !Global.SsSecuritiesInXray.Contains(item.Security)) - { - continue; - } - if (item.ConfigType == EConfigType.VLESS - && !Global.Flows.Contains(item.Flow)) - { - continue; - } - if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan - && item.StreamSecurity == Global.StreamSecurityReality - && item.PublicKey.IsNullOrEmpty()) - { - continue; - } - //inbound Inbounds4Ray inbound = new() { @@ -321,6 +344,7 @@ public partial class CoreConfigV2rayService(Config config) inbound.tag = inbound.protocol + inbound.port.ToString(); v2rayConfig.inbounds.Add(inbound); + //outbound var outbound = JsonUtils.Deserialize(txtOutbound); await GenOutbound(item, outbound); outbound.tag = Global.ProxyTag + inbound.port.ToString(); @@ -354,7 +378,8 @@ public partial class CoreConfigV2rayService(Config config) var ret = new RetResult(); try { - if (node is not { Port: > 0 }) + if (node == null + || !node.IsValid()) { ret.Msg = ResUI.CheckServerSettings; return ret; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs index 8d2476e6..660cc700 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs @@ -2,34 +2,87 @@ namespace ServiceLib.Services.CoreConfig; public partial class CoreConfigV2rayService { - private async Task GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad) + private async Task GenObservatory(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag) { - if (multipleLoad == EMultipleLoad.LeastPing) + // Collect all existing subject selectors from both observatories + var subjectSelectors = new List(); + subjectSelectors.AddRange(v2rayConfig.burstObservatory?.subjectSelector ?? []); + subjectSelectors.AddRange(v2rayConfig.observatory?.subjectSelector ?? []); + + // Case 1: exact match already exists -> nothing to do + if (subjectSelectors.Any(baseTagName.StartsWith)) + return await Task.FromResult(0); + + // Case 2: prefix match exists -> reuse it and move to the first position + var matched = subjectSelectors.FirstOrDefault(s => s.StartsWith(baseTagName)); + if (matched is not null) { - var observatory = new Observatory4Ray + baseTagName = matched; + + if (v2rayConfig.burstObservatory?.subjectSelector?.Contains(baseTagName) == true) { - subjectSelector = [Global.ProxyTag], - probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, - probeInterval = "3m", - enableConcurrency = true, - }; - v2rayConfig.observatory = observatory; + v2rayConfig.burstObservatory.subjectSelector.Remove(baseTagName); + v2rayConfig.burstObservatory.subjectSelector.Insert(0, baseTagName); + } + + if (v2rayConfig.observatory?.subjectSelector?.Contains(baseTagName) == true) + { + v2rayConfig.observatory.subjectSelector.Remove(baseTagName); + v2rayConfig.observatory.subjectSelector.Insert(0, baseTagName); + } + + return await Task.FromResult(0); } - else if (multipleLoad == EMultipleLoad.LeastLoad) + + // Case 3: need to create or insert based on multipleLoad type + if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback) { - var burstObservatory = new BurstObservatory4Ray + if (v2rayConfig.burstObservatory is null) { - subjectSelector = [Global.ProxyTag], - pingConfig = new() + // Create new burst observatory with default ping config + v2rayConfig.burstObservatory = new BurstObservatory4Ray { - destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, - interval = "5m", - timeout = "30s", - sampling = 2, - } - }; - v2rayConfig.burstObservatory = burstObservatory; + subjectSelector = [baseTagName], + pingConfig = new() + { + destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, + interval = "5m", + timeout = "30s", + sampling = 2, + } + }; + } + else + { + v2rayConfig.burstObservatory.subjectSelector ??= new(); + v2rayConfig.burstObservatory.subjectSelector.Add(baseTagName); + } } + else if (multipleLoad is EMultipleLoad.LeastPing) + { + if (v2rayConfig.observatory is null) + { + // Create new observatory with default probe config + v2rayConfig.observatory = new Observatory4Ray + { + subjectSelector = [baseTagName], + probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, + probeInterval = "3m", + enableConcurrency = true, + }; + } + else + { + v2rayConfig.observatory.subjectSelector ??= new(); + v2rayConfig.observatory.subjectSelector.Add(baseTagName); + } + } + + return await Task.FromResult(0); + } + + private async Task GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string selector = Global.ProxyTag) + { var strategyType = multipleLoad switch { EMultipleLoad.Random => "random", @@ -38,13 +91,22 @@ public partial class CoreConfigV2rayService EMultipleLoad.LeastLoad => "leastLoad", _ => "roundRobin", }; + var balancerTag = $"{selector}{Global.BalancerTagSuffix}"; var balancer = new BalancersItem4Ray { - selector = [Global.ProxyTag], - strategy = new() { type = strategyType }, - tag = $"{Global.ProxyTag}-round", + selector = [selector], + strategy = new() + { + type = strategyType, + settings = new() + { + expected = 1, + }, + }, + tag = balancerTag, }; - v2rayConfig.routing.balancers = [balancer]; - return await Task.FromResult(0); + v2rayConfig.routing.balancers ??= new(); + v2rayConfig.routing.balancers.Add(balancer); + return await Task.FromResult(balancerTag); } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index 11e8a8fa..58e4a840 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -480,6 +480,58 @@ public partial class CoreConfigV2rayService 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; + } + var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId); + if (hasCycle) + { + return -1; + } + + var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + 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 + if (node.ConfigType == EConfigType.PolicyGroup) + { + 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 @@ -552,7 +604,7 @@ public partial class CoreConfigV2rayService return 0; } - private async Task GenOutboundsList(List nodes, V2rayConfig v2rayConfig) + private async Task GenOutboundsListWithChain(List nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag) { try { @@ -577,6 +629,25 @@ public partial class CoreConfigV2rayService { index++; + if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + 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); @@ -590,7 +661,7 @@ public partial class CoreConfigV2rayService // current proxy await GenOutbound(node, currentOutbound); - currentOutbound.tag = $"{Global.ProxyTag}-{index}"; + currentOutbound.tag = $"{baseTagName}-{index}"; if (!node.Subid.IsNullOrEmpty()) { @@ -606,7 +677,7 @@ public partial class CoreConfigV2rayService { var prevOutbound = JsonUtils.Deserialize(txtOutbound); await GenOutbound(prevNode, prevOutbound); - prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}"; + prevTag = $"prev-{baseTagName}-{++prevIndex}"; prevOutbound.tag = prevTag; prevOutbounds.Add(prevOutbound); } @@ -628,9 +699,17 @@ public partial class CoreConfigV2rayService } // Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds - resultOutbounds.AddRange(prevOutbounds); - resultOutbounds.AddRange(v2rayConfig.outbounds); - v2rayConfig.outbounds = resultOutbounds; + if (baseTagName == Global.ProxyTag) + { + resultOutbounds.AddRange(prevOutbounds); + resultOutbounds.AddRange(v2rayConfig.outbounds); + v2rayConfig.outbounds = resultOutbounds; + } + else + { + v2rayConfig.outbounds.AddRange(prevOutbounds); + v2rayConfig.outbounds.AddRange(resultOutbounds); + } } catch (Exception ex) { @@ -692,4 +771,110 @@ public partial class CoreConfigV2rayService } 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]; + if (node == null) + continue; + if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId); + if (childProfiles.Count <= 0) + { + continue; + } + var childBaseTagName = $"{baseTagName}-{i + 1}"; + var ret = node.ConfigType switch + { + EConfigType.PolicyGroup => + await GenOutboundsListWithChain(childProfiles, v2rayConfig, childBaseTagName), + EConfigType.ProxyChain => + await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName), + _ => throw new NotImplementedException() + }; + continue; + } + 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); + } + if (baseTagName == Global.ProxyTag) + { + resultOutbounds.AddRange(v2rayConfig.outbounds); + v2rayConfig.outbounds = resultOutbounds; + } + else + { + v2rayConfig.outbounds.AddRange(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); + } + if (baseTagName == Global.ProxyTag) + { + resultOutbounds.AddRange(v2rayConfig.outbounds); + v2rayConfig.outbounds = resultOutbounds; + } + else + { + v2rayConfig.outbounds.AddRange(resultOutbounds); + } + + return await Task.FromResult(0); + } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs index d39ea833..e4a67410 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs @@ -125,18 +125,30 @@ public partial class CoreConfigV2rayService } var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); + if (node == null - || !Global.XraySupportConfigType.Contains(node.ConfigType)) + || (!Global.XraySupportConfigType.Contains(node.ConfigType) + && node.ConfigType is not (EConfigType.PolicyGroup or EConfigType.ProxyChain))) { return Global.ProxyTag; } - var tag = Global.ProxyTag + node.IndexId.ToString(); + var tag = $"{node.IndexId}-{Global.ProxyTag}"; if (v2rayConfig.outbounds.Any(p => p.tag == tag)) { return tag; } + if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + var ret = await GenGroupOutbound(node, v2rayConfig, tag); + if (ret == 0) + { + return tag; + } + return Global.ProxyTag; + } + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); var outbound = JsonUtils.Deserialize(txtOutbound); await GenOutbound(node, outbound); diff --git a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs new file mode 100644 index 00000000..beda67d0 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs @@ -0,0 +1,225 @@ +using System.Reactive; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace ServiceLib.ViewModels; + +public class AddGroupServerViewModel : MyReactiveObject +{ + [Reactive] + public ProfileItem SelectedSource { get; set; } + + [Reactive] + public ProfileItem SelectedChild { get; set; } + + [Reactive] + public IList SelectedChildren { get; set; } + + [Reactive] + public string? CoreType { get; set; } + + [Reactive] + public string? PolicyGroupType { get; set; } + + public IObservableCollection ChildItemsObs { get; } = new ObservableCollectionExtended(); + + //public ReactiveCommand AddCmd { get; } + public ReactiveCommand RemoveCmd { get; } + public ReactiveCommand MoveTopCmd { get; } + public ReactiveCommand MoveUpCmd { get; } + public ReactiveCommand MoveDownCmd { get; } + public ReactiveCommand MoveBottomCmd { get; } + + public ReactiveCommand SaveCmd { get; } + + public AddGroupServerViewModel(ProfileItem profileItem, Func>? updateView) + { + _config = AppManager.Instance.Config; + _updateView = updateView; + + var canEditRemove = this.WhenAnyValue( + x => x.SelectedChild, + SelectedChild => SelectedChild != null && !SelectedChild.Remarks.IsNullOrEmpty()); + + RemoveCmd = ReactiveCommand.CreateFromTask(async () => + { + await ChildRemoveAsync(); + }, canEditRemove); + MoveTopCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Top); + }, canEditRemove); + MoveUpCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Up); + }, canEditRemove); + MoveDownCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Down); + }, canEditRemove); + MoveBottomCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Bottom); + }, canEditRemove); + SaveCmd = ReactiveCommand.CreateFromTask(async () => + { + await SaveServerAsync(); + }); + + SelectedSource = profileItem.IndexId.IsNullOrEmpty() ? profileItem : JsonUtils.DeepCopy(profileItem); + + CoreType = (SelectedSource?.CoreType ?? ECoreType.Xray).ToString(); + + ProfileGroupItemManager.Instance.TryGet(profileItem.IndexId, out var profileGroup); + PolicyGroupType = (profileGroup?.MultipleLoad ?? EMultipleLoad.LeastPing) switch + { + EMultipleLoad.LeastPing => ResUI.TbLeastPing, + EMultipleLoad.Fallback => ResUI.TbFallback, + EMultipleLoad.Random => ResUI.TbRandom, + EMultipleLoad.RoundRobin => ResUI.TbRoundRobin, + EMultipleLoad.LeastLoad => ResUI.TbLeastLoad, + _ => ResUI.TbLeastPing, + }; + + _ = Init(); + } + + public async Task Init() + { + var childItemMulti = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource?.IndexId); + if (childItemMulti != null) + { + var childIndexIds = childItemMulti.ChildItems.IsNullOrEmpty() ? new List() : Utils.String2List(childItemMulti.ChildItems); + foreach (var item in childIndexIds) + { + var child = await AppManager.Instance.GetProfileItem(item); + if (child == null) + { + continue; + } + ChildItemsObs.Add(child); + } + } + } + + public async Task ChildRemoveAsync() + { + if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return; + } + foreach (var it in SelectedChildren ?? [SelectedChild]) + { + if (it != null) + { + ChildItemsObs.Remove(it); + } + } + await Task.CompletedTask; + } + + public async Task MoveServer(EMove eMove) + { + if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); + return; + } + var index = ChildItemsObs.IndexOf(SelectedChild); + if (index < 0) + { + return; + } + var selectedChild = JsonUtils.DeepCopy(SelectedChild); + switch (eMove) + { + case EMove.Top: + if (index == 0) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Insert(0, selectedChild); + break; + case EMove.Up: + if (index == 0) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Insert(index - 1, selectedChild); + break; + case EMove.Down: + if (index == ChildItemsObs.Count - 1) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Insert(index + 1, selectedChild); + break; + case EMove.Bottom: + if (index == ChildItemsObs.Count - 1) + { + return; + } + ChildItemsObs.RemoveAt(index); + ChildItemsObs.Add(selectedChild); + break; + default: + break; + } + await Task.CompletedTask; + } + + private async Task SaveServerAsync() + { + var remarks = SelectedSource.Remarks; + if (remarks.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks); + return; + } + if (ChildItemsObs.Count == 0) + { + NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer); + return; + } + SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? ECoreType.Xray : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); + if (SelectedSource.CoreType is not (ECoreType.Xray or ECoreType.sing_box) || + SelectedSource.ConfigType is not (EConfigType.ProxyChain or EConfigType.PolicyGroup)) + { + return; + } + var childIndexIds = new List(); + foreach (var item in ChildItemsObs) + { + if (item.IndexId.IsNullOrEmpty()) + { + continue; + } + childIndexIds.Add(item.IndexId); + } + var profileGroup = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource.IndexId); + profileGroup.ChildItems = Utils.List2String(childIndexIds); + profileGroup.MultipleLoad = PolicyGroupType switch + { + var s when s == ResUI.TbLeastPing => EMultipleLoad.LeastPing, + var s when s == ResUI.TbFallback => EMultipleLoad.Fallback, + var s when s == ResUI.TbRandom => EMultipleLoad.Random, + var s when s == ResUI.TbRoundRobin => EMultipleLoad.RoundRobin, + var s when s == ResUI.TbLeastLoad => EMultipleLoad.LeastLoad, + _ => EMultipleLoad.LeastPing, + }; + if (await ConfigHandler.AddGroupServerCommon(_config, SelectedSource, profileGroup, true) == 0) + { + NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); + _updateView?.Invoke(EViewAction.CloseWindow, null); + } + else + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + } + } +} diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index a6a6bd31..06ca3184 100644 --- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -23,6 +23,8 @@ public class MainWindowViewModel : MyReactiveObject public ReactiveCommand AddWireguardServerCmd { get; } public ReactiveCommand AddAnytlsServerCmd { get; } public ReactiveCommand AddCustomServerCmd { get; } + public ReactiveCommand AddPolicyGroupServerCmd { get; } + public ReactiveCommand AddProxyChainServerCmd { get; } public ReactiveCommand AddServerViaClipboardCmd { get; } public ReactiveCommand AddServerViaScanCmd { get; } public ReactiveCommand AddServerViaImageCmd { get; } @@ -122,6 +124,14 @@ public class MainWindowViewModel : MyReactiveObject { await AddServerAsync(true, EConfigType.Custom); }); + AddPolicyGroupServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.PolicyGroup); + }); + AddProxyChainServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddServerAsync(true, EConfigType.ProxyChain); + }); AddServerViaClipboardCmd = ReactiveCommand.CreateFromTask(async () => { await AddServerViaClipboardAsync(null); @@ -252,6 +262,7 @@ public class MainWindowViewModel : MyReactiveObject await ConfigHandler.InitBuiltinDNS(_config); await ConfigHandler.InitBuiltinFullConfigTemplate(_config); await ProfileExManager.Instance.Init(); + await ProfileGroupItemManager.Instance.Init(); await CoreManager.Instance.Init(_config, UpdateHandler); TaskManager.Instance.RegUpdateTask(_config, UpdateTaskHandler); @@ -340,6 +351,10 @@ public class MainWindowViewModel : MyReactiveObject { ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); } + else if (eConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); + } else { ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index b36c933c..20cc85ac 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -52,11 +52,13 @@ public class ProfilesViewModel : MyReactiveObject public ReactiveCommand CopyServerCmd { get; } public ReactiveCommand SetDefaultServerCmd { get; } public ReactiveCommand ShareServerCmd { get; } - public ReactiveCommand SetDefaultMultipleServerXrayRandomCmd { get; } - public ReactiveCommand SetDefaultMultipleServerXrayRoundRobinCmd { get; } - public ReactiveCommand SetDefaultMultipleServerXrayLeastPingCmd { get; } - public ReactiveCommand SetDefaultMultipleServerXrayLeastLoadCmd { get; } - public ReactiveCommand SetDefaultMultipleServerSingBoxLeastPingCmd { get; } + public ReactiveCommand GenGroupMultipleServerXrayRandomCmd { get; } + public ReactiveCommand GenGroupMultipleServerXrayRoundRobinCmd { get; } + public ReactiveCommand GenGroupMultipleServerXrayLeastPingCmd { get; } + public ReactiveCommand GenGroupMultipleServerXrayLeastLoadCmd { get; } + public ReactiveCommand GenGroupMultipleServerXrayFallbackCmd { get; } + public ReactiveCommand GenGroupMultipleServerSingBoxLeastPingCmd { get; } + public ReactiveCommand GenGroupMultipleServerSingBoxFallbackCmd { get; } //servers move public ReactiveCommand MoveTopCmd { get; } @@ -138,25 +140,33 @@ public class ProfilesViewModel : MyReactiveObject { await ShareServerAsync(); }, canEditRemove); - SetDefaultMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () => + GenGroupMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () => { - await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.Random); + await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Random); }, canEditRemove); - SetDefaultMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () => + GenGroupMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () => { - await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin); + await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin); }, canEditRemove); - SetDefaultMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () => + GenGroupMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () => { - await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing); + await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing); }, canEditRemove); - SetDefaultMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () => + GenGroupMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () => { - await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad); + await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad); }, canEditRemove); - SetDefaultMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () => + GenGroupMultipleServerXrayFallbackCmd = ReactiveCommand.CreateFromTask(async () => { - await SetDefaultMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing); + await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Fallback); + }, canEditRemove); + GenGroupMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () => + { + await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing); + }, canEditRemove); + GenGroupMultipleServerSingBoxFallbackCmd = ReactiveCommand.CreateFromTask(async () => + { + await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.Fallback); }, canEditRemove); //servers move @@ -500,6 +510,10 @@ public class ProfilesViewModel : MyReactiveObject { ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); } + else if (eConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); + } else { ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); @@ -615,7 +629,7 @@ public class ProfilesViewModel : MyReactiveObject await _updateView?.Invoke(EViewAction.ShareServer, url); } - private async Task SetDefaultMultipleServer(ECoreType coreType, EMultipleLoad multipleLoad) + private async Task GenGroupMultipleServer(ECoreType coreType, EMultipleLoad multipleLoad) { var lstSelected = await GetProfileItems(true); if (lstSelected == null) @@ -623,7 +637,7 @@ public class ProfilesViewModel : MyReactiveObject return; } - var ret = await ConfigHandler.AddCustomServer4Multiple(_config, lstSelected, coreType, multipleLoad); + var ret = await ConfigHandler.AddGroupServer4Multiple(_config, lstSelected, coreType, multipleLoad, SelectedSub?.Id); if (ret.Success != true) { NoticeManager.Instance.Enqueue(ResUI.OperationFailed); diff --git a/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml new file mode 100644 index 00000000..e24fa67c --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml @@ -0,0 +1,151 @@ + + + + +