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 2c47d31d..e404d071 100644 --- a/v2rayN/ServiceLib/Enums/EViewAction.cs +++ b/v2rayN/ServiceLib/Enums/EViewAction.cs @@ -24,6 +24,7 @@ public enum EViewAction RoutingRuleDetailsWindow, AddServerWindow, AddServer2Window, + AddGroupServerWindow, DNSSettingWindow, RoutingSettingWindow, OptionSettingWindow, diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index a7c74c08..c6c3460a 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -40,6 +40,7 @@ public class Global public const string ProxySetLinuxShellFileName = NamespaceSample + "proxy_set_linux_sh"; public const string KillAsSudoOSXShellFileName = NamespaceSample + "kill_as_sudo_osx_sh"; public const string KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh"; + public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter"; public const string DefaultSecurity = "auto"; public const string DefaultNetwork = "tcp"; @@ -49,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 e488720f..431fb33d 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -113,6 +113,10 @@ public static class ConfigHandler config.ConstItem ??= new ConstItem(); config.SimpleDNSItem ??= InitBuiltinSimpleDNS(); + if (config.SimpleDNSItem.GlobalFakeIp is null) + { + config.SimpleDNSItem.GlobalFakeIp = true; + } config.SpeedTestItem ??= new(); if (config.SpeedTestItem.SpeedTestTimeout < 10) @@ -353,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); @@ -1070,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 @@ -1141,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 @@ -1149,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; } @@ -1203,7 +1251,7 @@ public static class ConfigHandler public static async Task GetPreSocksItem(Config config, ProfileItem node, ECoreType coreType) { ProfileItem? itemSocks = null; - if (node.ConfigType != EConfigType.Custom && coreType != ECoreType.sing_box && config.TunModeItem.EnableTun) + if (node.ConfigType != EConfigType.Custom && node.ConfigType < EConfigType.Group && coreType != ECoreType.sing_box && config.TunModeItem.EnableTun) { itemSocks = new ProfileItem() { @@ -1214,7 +1262,7 @@ public static class ConfigHandler Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks) }; } - else if ((node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0)) + else if ((node.ConfigType == EConfigType.Custom && node.ConfigType < EConfigType.Group && node.PreSocksPort > 0)) { var preCoreType = config.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray; itemSocks = new ProfileItem() @@ -2221,6 +2269,7 @@ public static class ConfigHandler UseSystemHosts = false, AddCommonHosts = true, FakeIP = false, + GlobalFakeIp = true, BlockBindingQuery = true, DirectDNS = Global.DomainDirectDNSAddress.FirstOrDefault(), RemoteDNS = Global.DomainRemoteDNSAddress.FirstOrDefault(), 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 33125289..8a86133f 100644 --- a/v2rayN/ServiceLib/Manager/AppManager.cs +++ b/v2rayN/ServiceLib/Manager/AppManager.cs @@ -67,6 +67,7 @@ public sealed class AppManager SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); SQLiteHelper.Instance.CreateTable(); + SQLiteHelper.Instance.CreateTable(); return true; } @@ -101,6 +102,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(); @@ -219,6 +221,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/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs index 148f2bd7..b9b305bb 100644 --- a/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/ConfigItems.cs @@ -260,6 +260,7 @@ public class SimpleDNSItem public bool? UseSystemHosts { get; set; } public bool? AddCommonHosts { get; set; } public bool? FakeIP { get; set; } + public bool? GlobalFakeIp { get; set; } public bool? BlockBindingQuery { get; set; } public string? DirectDNS { get; set; } public string? RemoteDNS { get; set; } 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..45c8df8d 100644 --- a/v2rayN/ServiceLib/Models/ProfileItem.cs +++ b/v2rayN/ServiceLib/Models/ProfileItem.cs @@ -39,11 +39,14 @@ public class ProfileItem : ReactiveObject > 1 => $"***{arrAddr.Last()}", _ => Address }; - summary += ConfigType switch + if (ConfigType is EConfigType.Custom or > EConfigType.Group) { - EConfigType.Custom => $"[{CoreType.ToString()}]{Remarks}", - _ => $"{Remarks}({addr}:{Port})" - }; + summary += $"[{CoreType.ToString()}]{Remarks}"; + } + else + { + summary += $"{Remarks}({addr}:{Port})"; + } return summary; } 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 6503b41c..483fb417 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -672,6 +672,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Add Child Configuration 的本地化字符串。 + /// + public static string menuAddChildServer { + get { + return ResourceManager.GetString("menuAddChildServer", resourceCulture); + } + } + /// /// 查找类似 Add a custom configuration Configuration 的本地化字符串。 /// @@ -699,6 +708,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Add Policy Group Configuration 的本地化字符串。 + /// + public static string menuAddPolicyGroupServer { + get { + return ResourceManager.GetString("menuAddPolicyGroupServer", resourceCulture); + } + } + + /// + /// 查找类似 Add Proxy Chain Configuration 的本地化字符串。 + /// + public static string menuAddProxyChainServer { + get { + return ResourceManager.GetString("menuAddProxyChainServer", resourceCulture); + } + } + /// /// 查找类似 Import Share Links from clipboard (Ctrl+V) 的本地化字符串。 /// @@ -951,6 +978,78 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Generate Policy Group from Multiple Profiles 的本地化字符串。 + /// + public static string menuGenGroupMultipleServer { + get { + return ResourceManager.GetString("menuGenGroupMultipleServer", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration Fallback by sing-box 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerSingBoxFallback { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxFallback", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration LeastPing by sing-box 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerSingBoxLeastPing { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxLeastPing", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration Fallback by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayFallback { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayFallback", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration LeastLoad by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayLeastLoad { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastLoad", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration LeastPing by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayLeastPing { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastPing", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration Random by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayRandom { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayRandom", resourceCulture); + } + } + + /// + /// 查找类似 Multi-Configuration RoundRobin by Xray 的本地化字符串。 + /// + public static string menuGenGroupMultipleServerXrayRoundRobin { + get { + return ResourceManager.GetString("menuGenGroupMultipleServerXrayRoundRobin", resourceCulture); + } + } + /// /// 查找类似 Global Hotkey Setting 的本地化字符串。 /// @@ -1320,6 +1419,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Remove Child Configuration 的本地化字符串。 + /// + public static string menuRemoveChildServer { + get { + return ResourceManager.GetString("menuRemoveChildServer", resourceCulture); + } + } + /// /// 查找类似 Remove duplicate Configurations 的本地化字符串。 /// @@ -1473,6 +1581,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Server List 的本地化字符串。 + /// + public static string menuServerList { + get { + return ResourceManager.GetString("menuServerList", resourceCulture); + } + } + /// /// 查找类似 Configurations 的本地化字符串。 /// @@ -1482,60 +1599,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Multi-Configuration to custom configuration 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServer { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServer", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration LeastPing by sing-box 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerSingBoxLeastPing { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerSingBoxLeastPing", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration LeastLoad by Xray 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerXrayLeastLoad { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerXrayLeastLoad", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration LeastPing by Xray 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerXrayLeastPing { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerXrayLeastPing", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration Random by Xray 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerXrayRandom { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerXrayRandom", resourceCulture); - } - } - - /// - /// 查找类似 Multi-Configuration RoundRobin by Xray 的本地化字符串。 - /// - public static string menuSetDefaultMultipleServerXrayRoundRobin { - get { - return ResourceManager.GetString("menuSetDefaultMultipleServerXrayRoundRobin", resourceCulture); - } - } - /// /// 查找类似 Set as active Configuration (Enter) 的本地化字符串。 /// @@ -1995,6 +2058,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Please Add At Least One Configuration 的本地化字符串。 + /// + public static string PleaseAddAtLeastOneServer { + get { + return ResourceManager.GetString("PleaseAddAtLeastOneServer", resourceCulture); + } + } + /// /// 查找类似 Please fill Remarks 的本地化字符串。 /// @@ -2301,15 +2373,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Apply to Proxy Domains Only 的本地化字符串。 - /// - public static string TbApplyProxyDomainsOnly { - get { - return ResourceManager.GetString("TbApplyProxyDomainsOnly", resourceCulture); - } - } - /// /// 查找类似 Auto refresh 的本地化字符串。 /// @@ -2382,6 +2445,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Policy Group 的本地化字符串。 + /// + public static string TbConfigTypePolicyGroup { + get { + return ResourceManager.GetString("TbConfigTypePolicyGroup", resourceCulture); + } + } + + /// + /// 查找类似 Proxy Chain 的本地化字符串。 + /// + public static string TbConfigTypeProxyChain { + get { + return ResourceManager.GetString("TbConfigTypeProxyChain", resourceCulture); + } + } + /// /// 查找类似 Confirm 的本地化字符串。 /// @@ -2526,6 +2607,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Applies globally by default, with built-in FakeIP filtering (sing-box only). 的本地化字符串。 + /// + public static string TbFakeIPTips { + get { + return ResourceManager.GetString("TbFakeIPTips", resourceCulture); + } + } + + /// + /// 查找类似 Fallback 的本地化字符串。 + /// + public static string TbFallback { + get { + return ResourceManager.GetString("TbFallback", resourceCulture); + } + } + /// /// 查找类似 Fingerprint 的本地化字符串。 /// @@ -2643,6 +2742,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Most Stable 的本地化字符串。 + /// + public static string TbLeastLoad { + get { + return ResourceManager.GetString("TbLeastLoad", resourceCulture); + } + } + + /// + /// 查找类似 Lowest Latency 的本地化字符串。 + /// + public static string TbLeastPing { + get { + return ResourceManager.GetString("TbLeastPing", resourceCulture); + } + } + /// /// 查找类似 Address (IPv4, IPv6) 的本地化字符串。 /// @@ -2697,6 +2814,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Policy Group Type 的本地化字符串。 + /// + public static string TbPolicyGroupType { + get { + return ResourceManager.GetString("TbPolicyGroupType", resourceCulture); + } + } + /// /// 查找类似 Port 的本地化字符串。 /// @@ -2769,6 +2895,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Random 的本地化字符串。 + /// + public static string TbRandom { + get { + return ResourceManager.GetString("TbRandom", resourceCulture); + } + } + /// /// 查找类似 v2ray Full Config Template 的本地化字符串。 /// @@ -2832,6 +2967,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Round Robin 的本地化字符串。 + /// + public static string TbRoundRobin { + get { + return ResourceManager.GetString("TbRoundRobin", resourceCulture); + } + } + /// /// 查找类似 socks: local port, socks2: second local port, socks3: LAN port 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 0c8ca393..9893d47d 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 @@ -1455,9 +1455,6 @@ DNS Hosts: ("domain1 ip1 ip2" per line) - - Apply to Proxy Domains Only - Basic DNS Settings @@ -1515,4 +1512,55 @@ Select Profile + + Applies globally by default, with built-in FakeIP filtering (sing-box only). + + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Server List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index 800fa00f..aa1a3b13 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 @@ -1455,9 +1455,6 @@ DNS Hosts: ("domain1 ip1 ip2" per line) - - Apply to Proxy Domains Only - Basic DNS Settings @@ -1515,4 +1512,55 @@ Select Profile + + Applies globally by default, with built-in FakeIP filtering (sing-box only). + + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Server List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 614c8092..85875a90 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 @@ -1455,9 +1455,6 @@ DNS Hosts: ("domain1 ip1 ip2" per line) - - Apply to Proxy Domains Only - Basic DNS Settings @@ -1515,4 +1512,55 @@ Select Profile + + Applies globally by default, with built-in FakeIP filtering (sing-box only). + + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Server List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 294d9f34..1dfa268e 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) @@ -1455,9 +1455,6 @@ DNS hosts: (каждая строка в формате "domain1 ip1 ip2") - - Применять только к доменам через прокси - Базовые настройки DNS @@ -1515,4 +1512,55 @@ Select Profile + + Applies globally by default, with built-in FakeIP filtering (sing-box only). + + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Server List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index 194a59e2..516c2f4a 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 @@ -1452,9 +1452,6 @@ DNS Hosts:(“域名1 ip1 ip2” 一行一个) - - 仅对代理域名生效 - DNS 基础设置 @@ -1512,4 +1509,55 @@ 选择配置文件 + + 默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效 + + + 请至少添加一个配置文件 + + + 策略组 + + + 链式代理 + + + 最低延迟 + + + 随机 + + + 负载均衡 + + + 最稳定 + + + 策略组类型 + + + 添加策略组配置文件 + + + 添加链式代理配置文件 + + + 添加子配置文件 + + + 删除子配置文件 + + + 服务器列表 + + + 故障转移 + + + 多配置文件故障转移 sing-box + + + 多配置文件故障转移 Xray + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index fa84c789..c041de1f 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 @@ -1452,9 +1452,6 @@ DNS Hosts: ("domain1 ip1 ip2" per line) - - Apply to Proxy Domains Only - Basic DNS Settings @@ -1512,4 +1509,55 @@ Select Profile + + Applies globally by default, with built-in FakeIP filtering (sing-box only). + + + Please Add At Least One Configuration + + + Policy Group + + + Proxy Chain + + + Lowest Latency + + + Random + + + Round Robin + + + Most Stable + + + Policy Group Type + + + Add Policy Group Configuration + + + Add Proxy Chain Configuration + + + Add Child Configuration + + + Remove Child Configuration + + + Server List + + + Fallback + + + Multi-Configuration Fallback by sing-box + + + Multi-Configuration Fallback by Xray + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Sample/singbox_fakeip_filter b/v2rayN/ServiceLib/Sample/singbox_fakeip_filter new file mode 100644 index 00000000..860ab0eb --- /dev/null +++ b/v2rayN/ServiceLib/Sample/singbox_fakeip_filter @@ -0,0 +1,92 @@ +{ + "domain": [ + "amobile.music.tc.qq.com", + "api-jooxtt.sanook.com", + "api.joox.com", + "aqqmusic.tc.qq.com", + "dl.stream.qqmusic.qq.com", + "ff.dorado.sdo.com", + "heartbeat.belkin.com", + "isure.stream.qqmusic.qq.com", + "joox.com", + "lens.l.google.com", + "localhost.ptlogin2.qq.com", + "localhost.sec.qq.com", + "mesu.apple.com", + "mobileoc.music.tc.qq.com", + "music.taihe.com", + "musicapi.taihe.com", + "na.b.g-tun.com", + "proxy.golang.org", + "ps.res.netease.com", + "shark007.net", + "songsearch.kugou.com", + "static.adtidy.org", + "streamoc.music.tc.qq.com", + "swcdn.apple.com", + "swdist.apple.com", + "swdownload.apple.com", + "swquery.apple.com", + "swscan.apple.com", + "trackercdn.kugou.com", + "xnotify.xboxlive.com" + ], + "domain_keyword": [ + "ntp", + "stun", + "time" + ], + "domain_regex": [ + "^[^.]+$", + "^[^.]+\\.[^.]+\\.xboxlive\\.com$", + "^localhost\\.[^.]+\\.weixin\\.qq\\.com$", + "^mijia\\scloud$", + "^xbox\\.[^.]+\\.microsoft\\.com$", + "^xbox\\.[^.]+\\.[^.]+\\.microsoft\\.com$" + ], + "domain_suffix": [ + "126.net", + "3gppnetwork.org", + "battle.net", + "battlenet.com.cn", + "cdn.nintendo.net", + "cmbchina.com", + "cmbimg.com", + "ff14.sdo.com", + "ffxiv.com", + "finalfantasyxiv.com", + "gcloudcs.com", + "home.arpa", + "invalid", + "kuwo.cn", + "lan", + "linksys.com", + "linksyssmartwifi.com", + "local", + "localdomain", + "localhost", + "market.xiaomi.com", + "mcdn.bilivideo.cn", + "media.dssott.com", + "msftconnecttest.com", + "msftncsi.com", + "music.163.com", + "music.migu.cn", + "n0808.com", + "nflxvideo.net", + "oray.com", + "orayimg.com", + "router.asus.com", + "sandai.net", + "square-enix.com", + "srv.nintendo.net", + "steamcontent.com", + "uu.163.com", + "wargaming.net", + "wggames.cn", + "wotgame.cn", + "wowsgame.cn", + "xiami.com", + "y.qq.com" + ] +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/ServiceLib.csproj b/v2rayN/ServiceLib/ServiceLib.csproj index ecbab780..7ee72196 100644 --- a/v2rayN/ServiceLib/ServiceLib.csproj +++ b/v2rayN/ServiceLib/ServiceLib.csproj @@ -44,6 +44,7 @@ + diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs index 4e24d8e2..4c89afec 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs @@ -15,6 +15,33 @@ public partial class CoreConfigSingboxService(Config config) var ret = new RetResult(); try { + if (node?.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); + if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty()) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + 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) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + switch (node.ConfigType) + { + case EConfigType.PolicyGroup: + return await GenerateClientMultipleLoadConfig(childProfiles, profileGroupItem.MultipleLoad); + case EConfigType.ProxyChain: + return await GenerateClientChainConfig(childProfiles); + } + } + if (node == null || node.Port <= 0) { @@ -344,7 +371,105 @@ public partial class CoreConfigSingboxService(Config config) } } - public async Task GenerateClientMultipleLoadConfig(List selecteds) + public async Task GenerateClientMultipleLoadConfig(List selecteds, EMultipleLoad multipleLoad) + { + 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 proxyProfiles = new List(); + foreach (var it in selecteds) + { + if (it.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + var itemGroup = await AppManager.Instance.GetProfileItem(it.IndexId); + proxyProfiles.Add(itemGroup); + } + 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) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + await GenOutboundsList(proxyProfiles, singboxConfig, multipleLoad); + + 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(List selecteds) { var ret = new RetResult(); try @@ -419,7 +544,7 @@ public partial class CoreConfigSingboxService(Config config) ret.Msg = ResUI.FailedGenDefaultConfiguration; return ret; } - await GenOutboundsList(proxyProfiles, singboxConfig); + await GenChainOutboundsList(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 2f0f8015..0779a750 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs @@ -33,6 +33,15 @@ public partial class CoreConfigSingboxService lastRule.Ip?.Contains("0.0.0.0/0") == true); } singboxConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag; + if ((!useDirectDns) && simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false) + { + singboxConfig.dns.rules.Add(new() + { + server = Global.SingboxFakeDNSTag, + query_type = new List { 1, 28 }, // A and AAAA + rewrite_ttl = 1, + }); + } // Tun2SocksAddress if (node != null && Utils.IsDomain(node.Address)) @@ -197,6 +206,28 @@ public partial class CoreConfigSingboxService }); } + if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == true) + { + var fakeipFilterRule = JsonUtils.Deserialize(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName)); + fakeipFilterRule.invert = true; + var rule4Fake = new Rule4Sbox + { + server = Global.SingboxFakeDNSTag, + type = "logical", + mode = "and", + rewrite_ttl = 1, + rules = new List + { + new() { + query_type = new List { 1, 28 }, // A and AAAA + }, + fakeipFilterRule, + } + }; + + singboxConfig.dns.rules.Add(rule4Fake); + } + var routing = await ConfigHandler.GetDefaultRouting(_config); if (routing == null) return 0; @@ -276,10 +307,12 @@ public partial class CoreConfigSingboxService } else { - if (simpleDNSItem.FakeIP == true) + if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false) { var rule4Fake = JsonUtils.DeepCopy(rule); rule4Fake.server = Global.SingboxFakeDNSTag; + rule4Fake.query_type = new List { 1, 28 }; // A and AAAA + rule4Fake.rewrite_ttl = 1; singboxConfig.dns.rules.Add(rule4Fake); } rule.server = Global.SingboxRemoteDNSTag; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs index 3f03b93c..d4832ca2 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs @@ -410,7 +410,7 @@ public partial class CoreConfigSingboxService return 0; } - private async Task GenOutboundsList(List nodes, SingboxConfig singboxConfig) + private async Task GenOutboundsList(List nodes, SingboxConfig singboxConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag) { try { @@ -438,6 +438,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 GenOutboundsList(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 +482,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 +499,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 +540,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 +611,52 @@ public partial class CoreConfigSingboxService } return null; } + + private async Task GenChainOutboundsList(List nodes, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag) + { + var resultOutbounds = new List(); + var resultEndpoints = new List(); // For endpoints + for (var i = 0; i < nodes.Count; i++) + { + var node = nodes[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 != nodes.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 24804c50..0fd4cb0f 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -337,19 +337,60 @@ public partial class CoreConfigSingboxService } var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); + if (node == null - || !Global.SingboxSupportConfigType.Contains(node.ConfigType)) + || (!Global.SingboxSupportConfigType.Contains(node.ConfigType) + && node.ConfigType is not (EConfigType.PolicyGroup or EConfigType.ProxyChain))) { return Global.ProxyTag; } + var tag = Global.ProxyTag + node.IndexId.ToString(); + if (singboxConfig.outbounds.Any(o => o.tag == tag) + || (singboxConfig.endpoints != null && singboxConfig.endpoints.Any(e => e.tag == tag))) + { + return tag; + } + + if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); + if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty()) + { + return Global.ProxyTag; + } + 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) + { + return Global.ProxyTag; + } + var childBaseTagName = $"{Global.ProxyTag}-{node.IndexId}"; + var ret = node.ConfigType switch + { + EConfigType.PolicyGroup => + await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName), + EConfigType.ProxyChain => + await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName), + _ => throw new NotImplementedException() + }; + if (ret == 0) + { + 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..5555e67e 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs @@ -15,6 +15,33 @@ public partial class CoreConfigV2rayService(Config config) var ret = new RetResult(); try { + if (node?.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); + if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty()) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + 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) + { + ret.Msg = ResUI.CheckServerSettings; + return ret; + } + switch (node.ConfigType) + { + case EConfigType.PolicyGroup: + return await GenerateClientMultipleLoadConfig(childProfiles, profileGroupItem.MultipleLoad); + case EConfigType.ProxyChain: + return await GenerateClientChainConfig(childProfiles); + } + } + if (node == null || node.Port <= 0) { @@ -75,6 +102,156 @@ public partial class CoreConfigV2rayService(Config config) { 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 proxyProfiles = new List(); + foreach (var it in selecteds) + { + if (it.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + var itemGroup = await AppManager.Instance.GetProfileItem(it.IndexId); + proxyProfiles.Add(itemGroup); + } + 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) + { + ret.Msg = ResUI.FailedGenDefaultConfiguration; + return ret; + } + await GenOutboundsList(proxyProfiles, v2rayConfig); + + //add balancers + await GenObservatory(v2rayConfig, multipleLoad); + await GenBalancer(v2rayConfig, multipleLoad); + + 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(List selecteds) + { + var ret = new RetResult(); + try { if (_config == null) @@ -148,41 +325,7 @@ public partial class CoreConfigV2rayService(Config config) 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" - }); - } + await GenChainOutboundsList(proxyProfiles, v2rayConfig); ret.Success = true; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs index 8d2476e6..f703e8d1 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayBalancerService.cs @@ -2,7 +2,7 @@ namespace ServiceLib.Services.CoreConfig; public partial class CoreConfigV2rayService { - private async Task GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad) + private async Task GenObservatory(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad) { if (multipleLoad == EMultipleLoad.LeastPing) { @@ -15,7 +15,7 @@ public partial class CoreConfigV2rayService }; v2rayConfig.observatory = observatory; } - else if (multipleLoad == EMultipleLoad.LeastLoad) + else if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback) { var burstObservatory = new BurstObservatory4Ray { @@ -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..f383c216 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -552,7 +552,7 @@ public partial class CoreConfigV2rayService return 0; } - private async Task GenOutboundsList(List nodes, V2rayConfig v2rayConfig) + private async Task GenOutboundsList(List nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag) { try { @@ -577,6 +577,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 GenOutboundsList(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 +618,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 +634,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 +720,50 @@ public partial class CoreConfigV2rayService } return null; } + + private async Task GenChainOutboundsList(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; + } + + if (i == 0) + { + outbound.tag = baseTagName; + } + else + { + // avoid v2ray observe + outbound.tag = "chain-" + baseTagName + i.ToString(); + } + + if (i != nodes.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 1cb46bf2..50862c66 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayRoutingService.cs @@ -125,16 +125,60 @@ public partial class CoreConfigV2rayService } var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); + if (node == null - || !Global.XraySupportConfigType.Contains(node.ConfigType)) + || (!Global.XraySupportConfigType.Contains(node.ConfigType) + && node.ConfigType is not (EConfigType.PolicyGroup or EConfigType.ProxyChain))) { return Global.ProxyTag; } + var tag = Global.ProxyTag + node.IndexId.ToString(); + if (v2rayConfig.outbounds.Any(p => p.tag == tag)) + { + return tag; + } + + if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) + { + ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); + if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty()) + { + return Global.ProxyTag; + } + 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) + { + return Global.ProxyTag; + } + var childBaseTagName = $"{Global.ProxyTag}-{node.IndexId}"; + var ret = node.ConfigType switch + { + EConfigType.PolicyGroup => + await GenOutboundsList(childProfiles, v2rayConfig, childBaseTagName), + EConfigType.ProxyChain => + await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName), + _ => throw new NotImplementedException() + }; + if (node.ConfigType == EConfigType.PolicyGroup) + { + await GenBalancer(v2rayConfig, profileGroupItem.MultipleLoad, childBaseTagName); + } + if (ret == 0) + { + return childBaseTagName; + } + return Global.ProxyTag; + } + var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); var outbound = JsonUtils.Deserialize(txtOutbound); await GenOutbound(node, outbound); - outbound.tag = Global.ProxyTag + node.IndexId.ToString(); + outbound.tag = tag; v2rayConfig.outbounds.Add(outbound); return outbound.tag; diff --git a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs new file mode 100644 index 00000000..f4aac7a3 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs @@ -0,0 +1,222 @@ +using System.Data; +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; + + RemoveCmd = ReactiveCommand.CreateFromTask(async () => + { + await ChildRemoveAsync(); + }); + MoveTopCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Top); + }); + MoveUpCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Up); + }); + MoveDownCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Down); + }); + MoveBottomCmd = ReactiveCommand.CreateFromTask(async () => + { + await MoveServer(EMove.Bottom); + }); + 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 4e9fc9fc..1a180fbd 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); @@ -228,6 +238,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); @@ -322,6 +333,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 4c0fd2b9..8aff5950 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -53,11 +53,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; } @@ -139,25 +141,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 @@ -491,6 +501,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); @@ -606,7 +620,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) @@ -614,7 +628,7 @@ public class ProfilesViewModel : MyReactiveObject return; } - var ret = await ConfigHandler.AddCustomServer4Multiple(_config, lstSelected, coreType, multipleLoad); + var ret = await ConfigHandler.AddGroupServer4Multiple(_config, lstSelected, coreType, multipleLoad, SelectedSub?.Id); if (ret.Success != true) { NoticeManager.Instance.Enqueue(ResUI.OperationFailed); diff --git a/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml new file mode 100644 index 00000000..e24fa67c --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml @@ -0,0 +1,151 @@ + + + + +