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..81b0a9ff 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,35 @@ 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(); + } + 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 +1179,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 +1187,55 @@ 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 = string.Empty; 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, + Address = childProfileIndexId, + }; + 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 +1253,49 @@ 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) + { + static async Task> GetChildNodeAddressesAsync(string parentIndexId) + { + var childAddresses = new List(); + if (!ProfileGroupItemManager.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 GetChildNodeAddressesAsync(childNode.IndexId); + childAddresses.AddRange(subAddresses); + } + } + + return childAddresses; + } + + var lstAddresses = await GetChildNodeAddressesAsync(node.IndexId); + 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 2f5ccb64..f131adf8 100644 --- a/v2rayN/ServiceLib/Manager/AppManager.cs +++ b/v2rayN/ServiceLib/Manager/AppManager.cs @@ -65,6 +65,7 @@ public sealed class AppManager SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); + SQLiteHelper.Instance.CreateTable(); return true; } @@ -99,6 +100,7 @@ public sealed class AppManager await ConfigHandler.SaveConfig(_config); await ProfileExManager.Instance.SaveTo(); + await ProfileGroupItemManager.Instance.SaveTo(); await StatisticsManager.Instance.SaveTo(); await CoreManager.Instance.CoreStop(); StatisticsManager.Instance.Close(); @@ -223,6 +225,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..4812a0d2 --- /dev/null +++ b/v2rayN/ServiceLib/Manager/ProfileGroupItemManager.cs @@ -0,0 +1,167 @@ +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); + } + } +} diff --git a/v2rayN/ServiceLib/Manager/TaskManager.cs b/v2rayN/ServiceLib/Manager/TaskManager.cs index dfbd242d..c626e1b7 100644 --- a/v2rayN/ServiceLib/Manager/TaskManager.cs +++ b/v2rayN/ServiceLib/Manager/TaskManager.cs @@ -35,6 +35,7 @@ public class TaskManager await ConfigHandler.SaveConfig(_config); await ProfileExManager.Instance.SaveTo(); + await ProfileGroupItemManager.Instance.SaveTo(); } //Execute once 1 hour diff --git a/v2rayN/ServiceLib/Models/ProfileGroupItem.cs b/v2rayN/ServiceLib/Models/ProfileGroupItem.cs new file mode 100644 index 00000000..eb6ac49f --- /dev/null +++ b/v2rayN/ServiceLib/Models/ProfileGroupItem.cs @@ -0,0 +1,13 @@ +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..8f883535 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,87 @@ 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; + } + + public async Task HasCycle(HashSet visited, HashSet stack) + { + if (ConfigType < EConfigType.Group) + return false; + + if (stack.Contains(IndexId)) + return true; + + if (visited.Contains(IndexId)) + return false; + + visited.Add(IndexId); + stack.Add(IndexId); + + if (ProfileGroupItemManager.Instance.TryGet(IndexId, out var group) + && !group.ChildItems.IsNullOrEmpty()) + { + var childProfiles = (await Task.WhenAll( + Utils.String2List(group.ChildItems) + .Where(p => !p.IsNullOrEmpty()) + .Select(AppManager.Instance.GetProfileItem) + )) + .Where(p => p != null) + .ToList(); + + foreach (var child in childProfiles) + { + if (await child.HasCycle(visited, stack)) + return true; + } + } + + stack.Remove(IndexId); + return false; + } + #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 19ca4f0d..d841b89e 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -114,6 +114,33 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Core '{0}' does not support network type '{1}'. 的本地化字符串。 + /// + public static string CoreNotSupportNetwork { + get { + return ResourceManager.GetString("CoreNotSupportNetwork", resourceCulture); + } + } + + /// + /// 查找类似 Core '{0}' does not support protocol '{1}'. 的本地化字符串。 + /// + public static string CoreNotSupportProtocol { + get { + return ResourceManager.GetString("CoreNotSupportProtocol", resourceCulture); + } + } + + /// + /// 查找类似 Core '{0}' does not support protocol '{1}' when using transport '{2}'. 的本地化字符串。 + /// + public static string CoreNotSupportProtocolTransport { + get { + return ResourceManager.GetString("CoreNotSupportProtocolTransport", resourceCulture); + } + } + /// /// 查找类似 Note that custom configuration relies entirely on your own configuration and does not work with all settings. If you want to use the system proxy, please modify the listening port manually. 的本地化字符串。 /// @@ -267,6 +294,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Group '{0}' is empty. Please add at least one node. 的本地化字符串。 + /// + public static string GroupEmpty { + get { + return ResourceManager.GetString("GroupEmpty", resourceCulture); + } + } + + /// + /// 查找类似 The group "{0}" cannot reference itself. 的本地化字符串。 + /// + public static string GroupSelfReference { + get { + return ResourceManager.GetString("GroupSelfReference", resourceCulture); + } + } + /// /// 查找类似 This is not the correct configuration, please check 的本地化字符串。 /// @@ -294,6 +339,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 The {0} property is invalid, please check. 的本地化字符串。 + /// + public static string InvalidProperty { + get { + return ResourceManager.GetString("InvalidProperty", resourceCulture); + } + } + /// /// 查找类似 Invalid address (URL) 的本地化字符串。 /// @@ -672,6 +726,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Add Child Configuration 的本地化字符串。 + /// + public static string menuAddChildServer { + get { + return ResourceManager.GetString("menuAddChildServer", resourceCulture); + } + } + /// /// 查找类似 Add a custom configuration Configuration 的本地化字符串。 /// @@ -699,6 +762,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 +1032,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 +1473,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Remove Child Configuration 的本地化字符串。 + /// + public static string menuRemoveChildServer { + get { + return ResourceManager.GetString("menuRemoveChildServer", resourceCulture); + } + } + /// /// 查找类似 Remove duplicate Configurations 的本地化字符串。 /// @@ -1473,6 +1635,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Server List 的本地化字符串。 + /// + public static string menuServerList { + get { + return ResourceManager.GetString("menuServerList", resourceCulture); + } + } + /// /// 查找类似 Configurations 的本地化字符串。 /// @@ -1482,60 +1653,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) 的本地化字符串。 /// @@ -1941,6 +2058,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Node alias '{0}' does not exist. 的本地化字符串。 + /// + public static string NodeTagNotExist { + get { + return ResourceManager.GetString("NodeTagNotExist", resourceCulture); + } + } + /// /// 查找类似 Non-VMess or SS protocol 的本地化字符串。 /// @@ -1995,6 +2121,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Please Add At Least One Configuration 的本地化字符串。 + /// + public static string PleaseAddAtLeastOneServer { + get { + return ResourceManager.GetString("PleaseAddAtLeastOneServer", resourceCulture); + } + } + /// /// 查找类似 Please fill Remarks 的本地化字符串。 /// @@ -2040,6 +2175,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Policy group: 的本地化字符串。 + /// + public static string PolicyGroupPrefix { + get { + return ResourceManager.GetString("PolicyGroupPrefix", resourceCulture); + } + } + + /// + /// 查找类似 Proxy chained: 的本地化字符串。 + /// + public static string ProxyChainedPrefix { + get { + return ResourceManager.GetString("ProxyChainedPrefix", resourceCulture); + } + } + /// /// 查找类似 Global hotkey {0} registration failed, reason: {1} 的本地化字符串。 /// @@ -2103,6 +2256,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Routing rule outbound: 的本地化字符串。 + /// + public static string RoutingRuleOutboundPrefix { + get { + return ResourceManager.GetString("RoutingRuleOutboundPrefix", resourceCulture); + } + } + /// /// 查找类似 Run as Admin 的本地化字符串。 /// @@ -2373,6 +2535,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 +2706,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Fallback 的本地化字符串。 + /// + public static string TbFallback { + get { + return ResourceManager.GetString("TbFallback", resourceCulture); + } + } + /// /// 查找类似 Fingerprint 的本地化字符串。 /// @@ -2643,6 +2832,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 +2904,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Policy Group Type 的本地化字符串。 + /// + public static string TbPolicyGroupType { + get { + return ResourceManager.GetString("TbPolicyGroupType", resourceCulture); + } + } + /// /// 查找类似 Port 的本地化字符串。 /// @@ -2769,6 +2985,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Random 的本地化字符串。 + /// + public static string TbRandom { + get { + return ResourceManager.GetString("TbRandom", resourceCulture); + } + } + /// /// 查找类似 v2ray Full Config Template 的本地化字符串。 /// @@ -2832,6 +3057,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 7be84774..3e444836 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 @@ -1515,4 +1515,82 @@ 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 + + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + The group "{0}" cannot reference itself. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index a4701f0b..97a01bef 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 @@ -1515,4 +1515,82 @@ 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 + + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + The group "{0}" cannot reference itself. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 94e03415..4cc4f7f3 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 @@ -1515,4 +1515,82 @@ 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 + + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + The group "{0}" cannot reference itself. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index c72c83cf..985afaf3 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) @@ -1515,4 +1515,82 @@ 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 + + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + The group "{0}" cannot reference itself. + \ 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 0d5092d0..693ae828 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 @@ -1512,4 +1512,82 @@ 默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效 + + 请至少添加一个配置文件 + + + 策略组 + + + 链式代理 + + + 最低延迟 + + + 随机 + + + 负载均衡 + + + 最稳定 + + + 策略组类型 + + + 添加策略组配置文件 + + + 添加链式代理配置文件 + + + 添加子配置文件 + + + 删除子配置文件 + + + 服务器列表 + + + 故障转移 + + + 多配置文件故障转移 sing-box + + + 多配置文件故障转移 Xray + + + 核心 '{0}' 不支持网络类型 '{1}'。 + + + 核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}'。 + + + 核心 '{0}' 不支持协议 '{1}'。 + + + 代理链: + + + 路由规则出站: + + + 策略组: + + + 节点别名 '{0}' 不存在。 + + + 组“{0}”为空。请至少添加一个节点。 + + + {0}属性无效,请检查 + + + {0} 分组不能引用自身 + \ 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 4ad280b0..b6ed8be3 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 @@ -1512,4 +1512,82 @@ 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 + + + Core '{0}' does not support network type '{1}'. + + + Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + + Core '{0}' does not support protocol '{1}'. + + + Proxy chained: + + + Routing rule outbound: + + + Policy group: + + + Node alias '{0}' does not exist. + + + Group '{0}' is empty. Please add at least one node. + + + The {0} property is invalid, please check. + + + The group "{0}" cannot reference itself. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/ActionPrecheckService.cs b/v2rayN/ServiceLib/Services/ActionPrecheckService.cs new file mode 100644 index 00000000..2989936d --- /dev/null +++ b/v2rayN/ServiceLib/Services/ActionPrecheckService.cs @@ -0,0 +1,282 @@ +namespace ServiceLib.Services; + +/// +/// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.). +/// +public class ActionPrecheckService(Config config) +{ + private static readonly Lazy _instance = new(() => new ActionPrecheckService(AppManager.Instance.Config)); + public static ActionPrecheckService Instance => _instance.Value; + + private readonly Config _config = config; + + public async Task> CheckBeforeSetActive(string? indexId) + { + if (indexId.IsNullOrEmpty()) + { + return [ResUI.PleaseSelectServer]; + } + + var item = await AppManager.Instance.GetProfileItem(indexId); + if (item is null) + { + return [ResUI.PleaseSelectServer]; + } + + return await CheckBeforeGenerateConfig(item); + } + + public async Task> CheckBeforeGenerateConfig(ProfileItem? item) + { + if (item is null) + { + return [ResUI.PleaseSelectServer]; + } + + var errors = new List(); + + errors.AddRange(await ValidateCurrentNodeAndCoreSupport(item)); + errors.AddRange(await ValidateRelatedNodesExistAndValid(item)); + + return errors; + } + + private async Task> ValidateCurrentNodeAndCoreSupport(ProfileItem item) + { + if (item.ConfigType == EConfigType.Custom) + { + return []; + } + var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); + return await ValidateNodeAndCoreSupport(item, coreType); + } + + private async Task> ValidateNodeAndCoreSupport(ProfileItem item, ECoreType? coreType = null) + { + var errors = new List(); + + // sing-box does not support xhttp / kcp + // sing-box does not support transports like ws/http/httpupgrade/etc. when the node is not vmess/trojan/vless + coreType ??= AppManager.Instance.GetCoreType(item, item.ConfigType); + + if (item.ConfigType is EConfigType.Custom) + { + errors.Add(string.Format(ResUI.CoreNotSupportProtocol, coreType.ToString(), item.ConfigType.ToString())); + return errors; + } + + if (!item.IsComplex()) + { + if (item.Address.IsNullOrEmpty()) + { + errors.Add(string.Format(ResUI.InvalidProperty, "Address")); + return errors; + } + + if (item.Port is <= 0 or >= 65536) + { + errors.Add(string.Format(ResUI.InvalidProperty, "Port")); + return errors; + } + + + switch (item.ConfigType) + { + case EConfigType.VMess: + if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id)) + errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + break; + + case EConfigType.VLESS: + if (item.Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(item.Id) && item.Id.Length > 30)) + errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + if (!Global.Flows.Contains(item.Flow)) + errors.Add(string.Format(ResUI.InvalidProperty, "Flow")); + break; + + case EConfigType.Shadowsocks: + if (item.Id.IsNullOrEmpty()) + errors.Add(string.Format(ResUI.InvalidProperty, "Id")); + if (string.IsNullOrEmpty(item.Security) || !Global.SsSecuritiesInSingbox.Contains(item.Security)) + errors.Add(string.Format(ResUI.InvalidProperty, "Security")); + break; + } + + if ((item.ConfigType is EConfigType.VLESS or EConfigType.Trojan) + && item.StreamSecurity == Global.StreamSecurityReality + && item.PublicKey.IsNullOrEmpty()) + { + errors.Add(string.Format(ResUI.InvalidProperty, "PublicKey")); + } + + if (errors.Count > 0) + { + return errors; + } + } + + if (item.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + ProfileGroupItemManager.Instance.TryGet(item.IndexId, out var group); + if (group is null || group.ChildItems.IsNullOrEmpty()) + { + errors.Add(string.Format(ResUI.GroupEmpty, item.Remarks)); + return errors; + } + + var hasCycle = await item.HasCycle(new HashSet(), new HashSet()); + if (hasCycle) + { + errors.Add(string.Format(ResUI.GroupSelfReference, item.Remarks)); + return errors; + } + + foreach (var child in Utils.String2List(group.ChildItems)) + { + var childErrors = new List(); + if (child.IsNullOrEmpty()) + { + continue; + } + + var childItem = await AppManager.Instance.GetProfileItem(child); + if (childItem is null) + { + childErrors.Add(string.Format(ResUI.NodeTagNotExist, child)); + continue; + } + + if (childItem.ConfigType is EConfigType.Custom or EConfigType.ProxyChain) + { + childErrors.Add(string.Format(ResUI.InvalidProperty, childItem.Remarks)); + continue; + } + + childErrors.AddRange(await ValidateNodeAndCoreSupport(childItem, coreType)); + errors.AddRange(childErrors); + } + return errors; + } + + var net = item.GetNetwork() ?? item.Network; + + if (coreType == ECoreType.sing_box) + { + if (net is nameof(ETransport.kcp) or nameof(ETransport.xhttp)) + { + errors.Add(string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net)); + return errors; + } + + if (item.ConfigType is not (EConfigType.VMess or EConfigType.VLESS or EConfigType.Trojan)) + { + if (net is nameof(ETransport.ws) or nameof(ETransport.http) or nameof(ETransport.h2) or nameof(ETransport.quic) or nameof(ETransport.httpupgrade)) + { + errors.Add(string.Format(ResUI.CoreNotSupportProtocolTransport, nameof(ECoreType.sing_box), item.ConfigType.ToString(), net)); + return errors; + } + } + } + else if (coreType is ECoreType.Xray) + { + // Xray core does not support these protocols + if (item.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls) + { + errors.Add(string.Format(ResUI.CoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType.ToString())); + return errors; + } + } + + return errors.Select(s => $"{item.Remarks}: {s}").ToList(); + } + + private async Task> ValidateRelatedNodesExistAndValid(ProfileItem? item) + { + var errors = new List(); + errors.AddRange(await ValidateProxyChainedNodeExistAndValid(item)); + errors.AddRange(await ValidateRoutingNodeExistAndValid(item)); + return errors; + } + + private async Task> ValidateProxyChainedNodeExistAndValid(ProfileItem? item) + { + var errors = new List(); + if (item is null) + { + return errors; + } + + // prev node and next node + var subItem = await AppManager.Instance.GetSubItem(item.Subid); + if (subItem is null) + { + return errors; + } + + var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); + var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); + + await CollectProxyChainedNodeValidation(prevNode, subItem.PrevProfile, coreType, errors); + await CollectProxyChainedNodeValidation(nextNode, subItem.NextProfile, coreType, errors); + + return errors; + } + + private async Task CollectProxyChainedNodeValidation(ProfileItem? node, string tag, ECoreType coreType, List errors) + { + if (node is not null) + { + var nodeErrors = await ValidateNodeAndCoreSupport(node, coreType); + errors.AddRange(nodeErrors.Select(s => ResUI.ProxyChainedPrefix + s)); + } + else if (tag.IsNotEmpty()) + { + errors.Add(ResUI.ProxyChainedPrefix + string.Format(ResUI.NodeTagNotExist, tag)); + } + } + + private async Task> ValidateRoutingNodeExistAndValid(ProfileItem? item) + { + var errors = new List(); + + if (item is null) + { + return errors; + } + + var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); + var routing = await ConfigHandler.GetDefaultRouting(_config); + if (routing == null) + { + return errors; + } + + var rules = JsonUtils.Deserialize>(routing.RuleSet); + foreach (var ruleItem in rules ?? []) + { + if (!ruleItem.Enabled) + { + continue; + } + + var outboundTag = ruleItem.OutboundTag; + if (outboundTag.IsNullOrEmpty() || Global.OutboundTags.Contains(outboundTag)) + { + continue; + } + + var tagItem = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); + if (tagItem is null) + { + errors.Add(ResUI.RoutingRuleOutboundPrefix + string.Format(ResUI.NodeTagNotExist, outboundTag)); + continue; + } + + var tagErrors = await ValidateNodeAndCoreSupport(tagItem, coreType); + errors.AddRange(tagErrors.Select(s => ResUI.RoutingRuleOutboundPrefix + s)); + } + + return errors; + } +} diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs index 4e24d8e2..757ad3a3 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,64 @@ public partial class CoreConfigSingboxService(Config config) } } - public async Task GenerateClientMultipleLoadConfig(List selecteds) + public async Task GenerateClientMultipleLoadConfig(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); + await GenRouting(singboxConfig); + await GenExperimental(singboxConfig); + + var groupRet = await GenGroupOutbound(parentNode, singboxConfig); + if (groupRet != 0) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + 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 @@ -378,48 +423,12 @@ public partial class CoreConfigSingboxService(Config config) 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 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..8bc93e71 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,72 @@ 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; + } + 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, 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 +484,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 +512,38 @@ public partial class CoreConfigSingboxService { 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, 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 +556,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 +573,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 +614,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, }; @@ -574,4 +685,114 @@ 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]; + 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); + } + singboxConfig.outbounds ??= new(); + resultOutbounds.AddRange(singboxConfig.outbounds); + singboxConfig.outbounds = resultOutbounds; + singboxConfig.endpoints ??= new(); + resultEndpoints.AddRange(singboxConfig.endpoints); + singboxConfig.endpoints = resultEndpoints; + 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); + } + } + singboxConfig.outbounds ??= new(); + resultOutbounds.AddRange(singboxConfig.outbounds); + singboxConfig.outbounds = resultOutbounds; + + singboxConfig.endpoints ??= new(); + resultEndpoints.AddRange(singboxConfig.endpoints); + singboxConfig.endpoints = resultEndpoints; + + 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..d9b271ff 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -368,8 +368,10 @@ 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; } @@ -381,13 +383,24 @@ public partial class CoreConfigSingboxService return tag; } + if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + var childBaseTagName = $"{Global.ProxyTag}-{node.IndexId}"; + var ret = await GenGroupOutbound(node, singboxConfig, childBaseTagName); + if (ret == 0) + { + return childBaseTagName; + } + 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 b6148580..1f46c81d 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,112 @@ public partial class CoreConfigV2rayService(Config config) } } - public async Task GenerateClientMultipleLoadConfig(List selecteds, EMultipleLoad multipleLoad) + public async Task GenerateClientMultipleLoadConfig(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); + await GenRouting(v2rayConfig); + await GenDns(null, v2rayConfig); + await GenStatistic(v2rayConfig); + + var groupRet = await GenGroupOutbound(parentNode, v2rayConfig); + if (groupRet != 0) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + + var defaultBalancerTag = $"{Global.ProxyTag}{Global.BalancerTagSuffix}"; + + //add rule + var rules = v2rayConfig.routing.rules; + if (rules?.Count > 0) + { + var balancerTagSet = v2rayConfig.routing.balancers + .Select(b => b.tag) + .ToHashSet(); + + foreach (var rule in rules) + { + 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) + { + v2rayConfig.routing.rules.Add(new() + { + ip = ["0.0.0.0/0", "::/0"], + balancerTag = defaultBalancerTag, + type = "field" + }); + } + else + { + v2rayConfig.routing.rules.Add(new() + { + network = "tcp,udp", + balancerTag = defaultBalancerTag, + type = "field" + }); + } + + ret.Success = true; + + ret.Data = await ApplyFullConfigTemplate(v2rayConfig, true); + 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(); @@ -107,82 +223,12 @@ public partial class CoreConfigV2rayService(Config config) 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); - - var balancer = v2rayConfig.routing.balancers.First(); - - //add rule - var rules = v2rayConfig.routing.rules.Where(t => t.outboundTag == Global.ProxyTag).ToList(); - if (rules?.Count > 0) - { - foreach (var rule in rules) - { - rule.outboundTag = null; - rule.balancerTag = balancer.tag; - } - } - if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch) - { - v2rayConfig.routing.rules.Add(new() - { - ip = ["0.0.0.0/0", "::/0"], - balancerTag = balancer.tag, - type = "field" - }); - } - else - { - v2rayConfig.routing.rules.Add(new() - { - network = "tcp,udp", - balancerTag = balancer.tag, - type = "field" - }); - } ret.Success = true; @@ -255,12 +301,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 +332,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 +342,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 +376,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..028782e9 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs @@ -2,24 +2,24 @@ 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) { var observatory = new Observatory4Ray { - subjectSelector = [Global.ProxyTag], + subjectSelector = [baseTagName], probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, probeInterval = "3m", enableConcurrency = true, }; v2rayConfig.observatory = observatory; } - else if (multipleLoad == EMultipleLoad.LeastLoad) + else if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback) { var burstObservatory = new BurstObservatory4Ray { - subjectSelector = [Global.ProxyTag], + subjectSelector = [baseTagName], pingConfig = new() { destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl, @@ -30,6 +30,11 @@ public partial class CoreConfigV2rayService }; v2rayConfig.burstObservatory = burstObservatory; } + 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 +43,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..3d6597ae 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -480,6 +480,76 @@ 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; + } + 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 @@ -552,7 +622,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 +647,34 @@ public partial class CoreConfigV2rayService { 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); @@ -590,7 +688,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 +704,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); } @@ -692,4 +790,78 @@ 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]; + 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); + } } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs index d39ea833..a82c5974 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs @@ -125,8 +125,10 @@ 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; } @@ -137,6 +139,17 @@ public partial class CoreConfigV2rayService return tag; } + if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + var childBaseTagName = $"{Global.ProxyTag}-{node.IndexId}"; + var ret = await GenGroupOutbound(node, v2rayConfig, childBaseTagName); + if (ret == 0) + { + return childBaseTagName; + } + 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..4a6af40e --- /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()) + { + childIndexIds.Add(item.IndexId); + } + } + SelectedSource.Address = Utils.List2String(childIndexIds); + 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..546171b2 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); @@ -591,6 +605,16 @@ public class ProfilesViewModel : MyReactiveObject return; } + var msgs = await ActionPrecheckService.Instance.CheckBeforeSetActive(indexId); + foreach (var msg in msgs) + { + NoticeManager.Instance.SendMessage(msg); + } + if (msgs.Count > 0) + { + NoticeManager.Instance.Enqueue(msgs.First()); + } + if (await ConfigHandler.SetDefaultServerIndex(_config, indexId) == 0) { await RefreshServers(); @@ -615,7 +639,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 +647,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); @@ -754,6 +778,16 @@ public class ProfilesViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } + + var msgs = await ActionPrecheckService.Instance.CheckBeforeGenerateConfig(item); + foreach (var msg in msgs) + { + NoticeManager.Instance.SendMessage(msg); + } + if (msgs.Count > 0) + { + NoticeManager.Instance.Enqueue(msgs.First()); + } if (blClipboard) { var result = await CoreConfigHandler.GenerateClientConfig(item, null); 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 @@ + + + + +