Compare commits

..

26 commits

Author SHA1 Message Date
DHR60
2feb14afad
Merge 6250810f31 into d7c5161431 2025-10-02 20:13:25 +08:00
DHR60
6250810f31 PreCheck 2025-09-29 17:19:41 +08:00
DHR60
4bf86665b7 Avoids circular dependency in profile groups
Adds cycle detection to prevent infinite loops when evaluating profile groups.

This ensures that profile group configurations don't result in stack overflow errors when groups reference each other, directly or indirectly.
2025-09-29 17:19:35 +08:00
DHR60
ae46b39110 Improves Tun2Socks address handling 2025-09-29 16:43:57 +08:00
DHR60
836217266e Fix 2025-09-29 16:43:56 +08:00
DHR60
4e3c2991f9 Avoid self-reference 2025-09-29 16:43:56 +08:00
DHR60
cdab7a097f Add chain selection control to group outbounds 2025-09-29 16:43:56 +08:00
DHR60
1d8b8491ea Refactor 2025-09-29 16:43:56 +08:00
DHR60
04dae54654 Add helper function 2025-09-29 16:43:56 +08:00
DHR60
63bf68376f Adjust chained proxy, actual outbound is at the top
Based on actual network flow instead of data packets
2025-09-29 16:43:56 +08:00
DHR60
826f9f833a Refactor 2025-09-29 16:43:56 +08:00
DHR60
9317c73084 Avoid duplicate tags 2025-09-29 16:43:56 +08:00
DHR60
2d5a5465df Add group in traffic splitting support 2025-09-29 16:43:56 +08:00
DHR60
af62d9e0f4 Add PolicyGroup include other Group support 2025-09-29 16:43:56 +08:00
DHR60
2ab044b4fc Add fallback support 2025-09-29 16:43:56 +08:00
DHR60
c76d125ce1 Fix 2025-09-29 16:43:56 +08:00
DHR60
183be53153 Add Proxy Chain support 2025-09-29 16:43:56 +08:00
DHR60
52d1eb1e2b Adjust UI 2025-09-29 16:43:56 +08:00
DHR60
521dca33d5 Add generate policy group 2025-09-29 16:43:56 +08:00
DHR60
a323484ee3 Add Policy Group support 2025-09-29 16:43:55 +08:00
DHR60
9a43003c47 Rename 2025-09-29 16:43:55 +08:00
DHR60
e97b98f4bf Exclude specific profile types from selection 2025-09-29 16:43:55 +08:00
DHR60
bd3a733057 Fix right click not working 2025-09-29 16:43:55 +08:00
DHR60
0cde086448 avalonia 2025-09-29 16:43:55 +08:00
DHR60
8561311a45 VM and wpf 2025-09-29 16:43:55 +08:00
DHR60
57fd56fc05 Multi Profile 2025-09-29 16:43:55 +08:00
44 changed files with 470 additions and 512 deletions

View file

@ -6,10 +6,10 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.7" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.7" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.7" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.7" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.6" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.6" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.6" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.6" />
<PackageVersion Include="CliWrap" Version="3.9.0" />
<PackageVersion Include="Downloader" Version="4.0.3" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" />
@ -19,9 +19,9 @@
<PackageVersion Include="ReactiveUI" Version="20.4.1" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.7" />
<PackageVersion Include="Semi.Avalonia" Version="11.2.1.10" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.2.1.10" />
<PackageVersion Include="NLog" Version="6.0.4" />
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />

View file

@ -85,19 +85,13 @@ public class Utils
/// Base64 Encode
/// </summary>
/// <param name="plainText"></param>
/// <param name="removePadding"></param>
/// <returns></returns>
public static string Base64Encode(string plainText, bool removePadding = false)
public static string Base64Encode(string plainText)
{
try
{
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
var base64 = Convert.ToBase64String(plainTextBytes);
if (removePadding)
{
base64 = base64.TrimEnd('=');
}
return base64;
return Convert.ToBase64String(plainTextBytes);
}
catch (Exception ex)
{
@ -118,7 +112,7 @@ public class Utils
{
if (plainText.IsNullOrEmpty())
{
return string.Empty;
return "";
}
plainText = plainText.Trim()
@ -953,7 +947,7 @@ public class Utils
if (SetUnixFileMode(fileName))
{
Logging.SaveLog($"Successfully set the file execution permission, {fileName}");
return string.Empty;
return "";
}
if (fileName.Contains(' '))

View file

@ -1087,8 +1087,6 @@ public static class ConfigHandler
profileItem.IndexId = Utils.GetGuid(false);
maxSort = ProfileExManager.Instance.GetMaxSort();
}
var groupType = profileItem.ConfigType == EConfigType.ProxyChain ? EConfigType.ProxyChain.ToString() : profileGroupItem.MultipleLoad.ToString();
profileItem.Address = $"{profileItem.CoreType}-{groupType}";
if (maxSort > 0)
{
ProfileExManager.Instance.SetSort(profileItem.IndexId, maxSort + 1);
@ -1196,10 +1194,10 @@ public static class ConfigHandler
var indexId = Utils.GetGuid(false);
var childProfileIndexId = Utils.List2String(selecteds.Select(p => p.IndexId).ToList());
var remark = subId.IsNullOrEmpty() ? string.Empty : $"{(await AppManager.Instance.GetSubItem(subId)).Remarks} ";
var remark = string.Empty;
if (coreType == ECoreType.Xray)
{
remark += multipleLoad switch
remark = multipleLoad switch
{
EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerXrayLeastPing,
EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerXrayFallback,
@ -1211,7 +1209,7 @@ public static class ConfigHandler
}
else if (coreType == ECoreType.sing_box)
{
remark += multipleLoad switch
remark = multipleLoad switch
{
EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerSingBoxLeastPing,
EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerSingBoxFallback,
@ -1224,6 +1222,7 @@ public static class ConfigHandler
CoreType = coreType,
ConfigType = EConfigType.PolicyGroup,
Remarks = remark,
Address = childProfileIndexId,
};
if (!subId.IsNullOrEmpty())
{
@ -1257,7 +1256,35 @@ public static class ConfigHandler
var tun2SocksAddress = node.Address;
if (node.ConfigType > EConfigType.Group)
{
var lstAddresses = (await ProfileGroupItemManager.GetAllChildDomainAddresses(node.IndexId)).ToList();
static async Task<List<string>> GetChildNodeAddressesAsync(string parentIndexId)
{
var childAddresses = new List<string>();
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);

View file

@ -155,60 +155,61 @@ public class BaseFmt
protected static int ResolveStdTransport(NameValueCollection query, ref ProfileItem item)
{
item.Flow = GetQueryValue(query, "flow");
item.StreamSecurity = GetQueryValue(query, "security");
item.Sni = GetQueryValue(query, "sni");
item.Alpn = GetQueryDecoded(query, "alpn");
item.Fingerprint = GetQueryDecoded(query, "fp");
item.PublicKey = GetQueryDecoded(query, "pbk");
item.ShortId = GetQueryDecoded(query, "sid");
item.SpiderX = GetQueryDecoded(query, "spx");
item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
item.AllowInsecure = new[] { "allowInsecure", "allow_insecure", "insecure" }.Any(k => (query[k] ?? "") == "1") ? "true" : "";
item.Flow = query["flow"] ?? "";
item.StreamSecurity = query["security"] ?? "";
item.Sni = query["sni"] ?? "";
item.Alpn = Utils.UrlDecode(query["alpn"] ?? "");
item.Fingerprint = Utils.UrlDecode(query["fp"] ?? "");
item.PublicKey = Utils.UrlDecode(query["pbk"] ?? "");
item.ShortId = Utils.UrlDecode(query["sid"] ?? "");
item.SpiderX = Utils.UrlDecode(query["spx"] ?? "");
item.Mldsa65Verify = Utils.UrlDecode(query["pqv"] ?? "");
item.AllowInsecure = (query["allowInsecure"] ?? "") == "1" ? "true" : "";
item.Network = GetQueryValue(query, "type", nameof(ETransport.tcp));
item.Network = query["type"] ?? nameof(ETransport.tcp);
switch (item.Network)
{
case nameof(ETransport.tcp):
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
item.RequestHost = GetQueryDecoded(query, "host");
item.HeaderType = query["headerType"] ?? Global.None;
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
break;
case nameof(ETransport.kcp):
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
item.Path = GetQueryDecoded(query, "seed");
item.HeaderType = query["headerType"] ?? Global.None;
item.Path = Utils.UrlDecode(query["seed"] ?? "");
break;
case nameof(ETransport.ws):
case nameof(ETransport.httpupgrade):
item.RequestHost = GetQueryDecoded(query, "host");
item.Path = GetQueryDecoded(query, "path", "/");
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
item.Path = Utils.UrlDecode(query["path"] ?? "/");
break;
case nameof(ETransport.xhttp):
item.RequestHost = GetQueryDecoded(query, "host");
item.Path = GetQueryDecoded(query, "path", "/");
item.HeaderType = GetQueryDecoded(query, "mode");
item.Extra = GetQueryDecoded(query, "extra");
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
item.Path = Utils.UrlDecode(query["path"] ?? "/");
item.HeaderType = Utils.UrlDecode(query["mode"] ?? "");
item.Extra = Utils.UrlDecode(query["extra"] ?? "");
break;
case nameof(ETransport.http):
case nameof(ETransport.h2):
item.Network = nameof(ETransport.h2);
item.RequestHost = GetQueryDecoded(query, "host");
item.Path = GetQueryDecoded(query, "path", "/");
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
item.Path = Utils.UrlDecode(query["path"] ?? "/");
break;
case nameof(ETransport.quic):
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
item.RequestHost = GetQueryValue(query, "quicSecurity", Global.None);
item.Path = GetQueryDecoded(query, "key");
item.HeaderType = query["headerType"] ?? Global.None;
item.RequestHost = query["quicSecurity"] ?? Global.None;
item.Path = Utils.UrlDecode(query["key"] ?? "");
break;
case nameof(ETransport.grpc):
item.RequestHost = GetQueryDecoded(query, "authority");
item.Path = GetQueryDecoded(query, "serviceName");
item.HeaderType = GetQueryDecoded(query, "mode", Global.GrpcGunMode);
item.RequestHost = Utils.UrlDecode(query["authority"] ?? "");
item.Path = Utils.UrlDecode(query["serviceName"] ?? "");
item.HeaderType = Utils.UrlDecode(query["mode"] ?? Global.GrpcGunMode);
break;
default:
@ -238,14 +239,4 @@ public class BaseFmt
var url = $"{Utils.UrlEncode(userInfo)}@{GetIpv6(address)}:{port}";
return $"{Global.ProtocolShares[eConfigType]}{url}{query}{remark}";
}
protected static string GetQueryValue(NameValueCollection query, string key, string defaultValue = "")
{
return query[key] ?? defaultValue;
}
protected static string GetQueryDecoded(NameValueCollection query, string key, string defaultValue = "")
{
return Utils.UrlDecode(GetQueryValue(query, key, defaultValue));
}
}

View file

@ -27,7 +27,7 @@ public class FmtHandler
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
return string.Empty;
return "";
}
}

View file

@ -21,10 +21,10 @@ public class Hysteria2Fmt : BaseFmt
var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item);
item.Path = GetQueryDecoded(query, "obfs-password");
item.AllowInsecure = GetQueryValue(query, "insecure") == "1" ? "true" : "false";
item.Path = Utils.UrlDecode(query["obfs-password"] ?? "");
item.AllowInsecure = (query["insecure"] ?? "") == "1" ? "true" : "false";
item.Ports = GetQueryDecoded(query, "mport");
item.Ports = Utils.UrlDecode(query["mport"] ?? "");
return item;
}

View file

@ -42,7 +42,7 @@ public class ShadowsocksFmt : BaseFmt
// item.port);
//url = Utile.Base64Encode(url);
//new Sip002
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}", true);
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}");
return ToUri(EConfigType.Shadowsocks, item.Address, item.Port, pw, null, remark);
}

View file

@ -33,7 +33,7 @@ public class SocksFmt : BaseFmt
remark = "#" + Utils.UrlEncode(item.Remarks);
}
//new
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}", true);
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}");
return ToUri(EConfigType.SOCKS, item.Address, item.Port, pw, null, remark);
}

View file

@ -30,7 +30,7 @@ public class TuicFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item);
item.HeaderType = GetQueryValue(query, "congestion_control");
item.HeaderType = query["congestion_control"] ?? "";
return item;
}

View file

@ -24,8 +24,8 @@ public class VLESSFmt : BaseFmt
item.Id = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query);
item.Security = GetQueryValue(query, "encryption", Global.None);
item.StreamSecurity = GetQueryValue(query, "security");
item.Security = query["encryption"] ?? Global.None;
item.StreamSecurity = query["security"] ?? "";
_ = ResolveStdTransport(query, ref item);
return item;

View file

@ -24,10 +24,10 @@ public class WireguardFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query);
item.PublicKey = GetQueryDecoded(query, "publickey");
item.Path = GetQueryDecoded(query, "reserved");
item.RequestHost = GetQueryDecoded(query, "address");
item.ShortId = GetQueryDecoded(query, "mtu");
item.PublicKey = Utils.UrlDecode(query["publickey"] ?? "");
item.Path = Utils.UrlDecode(query["reserved"] ?? "");
item.RequestHost = Utils.UrlDecode(query["address"] ?? "");
item.ShortId = Utils.UrlDecode(query["mtu"] ?? "");
return item;
}

View file

@ -8,6 +8,7 @@ public sealed class AppManager
private Config _config;
private int? _statePort;
private int? _statePort2;
private WindowsJob? _processJob;
public static AppManager Instance => _instance.Value;
public Config Config => _config;
@ -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();
@ -136,6 +138,21 @@ public sealed class AppManager
return localPort + (int)protocol;
}
public void AddProcess(nint processHandle)
{
if (Utils.IsWindows())
{
_processJob ??= new();
try
{
_processJob?.AddProcess(processHandle);
}
catch
{
}
}
}
#endregion Config
#region SqliteHelper

View file

@ -8,7 +8,6 @@ public class CoreManager
private static readonly Lazy<CoreManager> _instance = new(() => new());
public static CoreManager Instance => _instance.Value;
private Config _config;
private WindowsJob? _processJob;
private ProcessService? _processService;
private ProcessService? _processPreService;
private bool _linuxSudo = false;
@ -265,28 +264,14 @@ public class CoreManager
await procService.StartAsync();
await Task.Delay(100);
AppManager.Instance.AddProcess(procService.Handle);
if (procService is null or { HasExited: true })
{
throw new Exception(ResUI.FailedToRunCore);
}
AddProcessJob(procService.Handle);
return procService;
}
private void AddProcessJob(nint processHandle)
{
if (Utils.IsWindows())
{
_processJob ??= new();
try
{
_processJob?.AddProcess(processHandle);
}
catch { }
}
}
#endregion Process
}

View file

@ -164,113 +164,4 @@ public class ProfileGroupItemManager
Logging.SaveLog(_tag, ex);
}
}
#region Helper
public static bool HasCycle(string? indexId)
{
return HasCycle(indexId, new HashSet<string>(), new HashSet<string>());
}
public static bool HasCycle(string? indexId, HashSet<string> visited, HashSet<string> stack)
{
if (indexId.IsNullOrEmpty())
return false;
if (stack.Contains(indexId))
return true;
if (visited.Contains(indexId))
return false;
visited.Add(indexId);
stack.Add(indexId);
Instance.TryGet(indexId, out var groupItem);
if (groupItem == null || groupItem.ChildItems.IsNullOrEmpty())
{
return false;
}
var childIds = Utils.String2List(groupItem.ChildItems)
.Where(p => !string.IsNullOrEmpty(p))
.ToList();
foreach (var child in childIds)
{
if (HasCycle(child, visited, stack))
{
return true;
}
}
stack.Remove(indexId);
return false;
}
public static async Task<(List<ProfileItem> Items, ProfileGroupItem? Group)> GetChildProfileItems(string? indexId)
{
Instance.TryGet(indexId, out var profileGroupItem);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return (new List<ProfileItem>(), profileGroupItem);
}
var items = await GetChildProfileItems(profileGroupItem);
return (items, profileGroupItem);
}
public static async Task<List<ProfileItem>> GetChildProfileItems(ProfileGroupItem? group)
{
if (group == null || group.ChildItems.IsNullOrEmpty())
{
return new();
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(group.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
))
.Where(p =>
p != null &&
p.IsValid() &&
p.ConfigType != EConfigType.Custom
)
.ToList();
return childProfiles;
}
public static async Task<HashSet<string>> GetAllChildDomainAddresses(string parentIndexId)
{
// include grand children
var childAddresses = new HashSet<string>();
if (!Instance.TryGet(parentIndexId, out var groupItem) || groupItem.ChildItems.IsNullOrEmpty())
return childAddresses;
var childIds = Utils.String2List(groupItem.ChildItems);
foreach (var childId in childIds)
{
var childNode = await AppManager.Instance.GetProfileItem(childId);
if (childNode == null)
continue;
if (!childNode.IsComplex())
{
childAddresses.Add(childNode.Address);
}
else if (childNode.ConfigType > EConfigType.Group)
{
var subAddresses = await GetAllChildDomainAddresses(childNode.IndexId);
foreach (var addr in subAddresses)
{
childAddresses.Add(addr);
}
}
}
return childAddresses;
}
#endregion Helper
}

View file

@ -35,6 +35,7 @@ public class TaskManager
await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo();
await ProfileGroupItemManager.Instance.SaveTo();
}
//Execute once 1 hour

View file

@ -1,5 +1,4 @@
using SQLite;
namespace ServiceLib.Models;
[Serializable]

View file

@ -109,6 +109,42 @@ public class ProfileItem : ReactiveObject
return true;
}
public async Task<bool> HasCycle(HashSet<string> visited, HashSet<string> 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]

View file

@ -3166,7 +3166,16 @@ namespace ServiceLib.Resx {
}
/// <summary>
/// 查找类似 Resolve DNS server domains, requires IP 的本地化字符串。
/// 查找类似 The sing-box DoH resolution server can be overwritten 的本地化字符串。
/// </summary>
public static string TbSBDoHOverride {
get {
return ResourceManager.GetString("TbSBDoHOverride", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fallback DNS Resolution, Require IP 的本地化字符串。
/// </summary>
public static string TbSBFallbackDNSResolve {
get {

View file

@ -1429,7 +1429,7 @@
<value>Bootstrap DNS (sing-box)</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Resolve DNS server domains, requires IP</value>
<value>Fallback DNS Resolution, Require IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value>
@ -1443,6 +1443,9 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>The sing-box DoH resolution server can be overwritten</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>

View file

@ -1429,7 +1429,7 @@
<value>Bootstrap DNS (sing-box)</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Resolve DNS server domains, requires IP</value>
<value>Fallback DNS Resolution, Require IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value>
@ -1443,6 +1443,9 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>The sing-box DoH resolution server can be overwritten</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>

View file

@ -1429,7 +1429,7 @@
<value>Bootstrap DNS (sing-box)</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Resolve DNS server domains, requires IP</value>
<value>Fallback DNS Resolution, Require IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value>
@ -1443,6 +1443,9 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>The sing-box DoH resolution server can be overwritten</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>

View file

@ -1429,7 +1429,7 @@
<value>Bootstrap DNS (sing-box)</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Resolve DNS server domains, requires IP</value>
<value>Fallback DNS Resolution, Require IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>Стратегия резолвинга Freedom (Xray)</value>
@ -1443,6 +1443,9 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Добавить стандартные записи hosts (DNS)</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>Сервер DoH-резолвера sing-box можно переопределить</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>

View file

@ -1426,7 +1426,7 @@
<value>Bootstrap DNS (sing-box)</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>解析 DNS 服务器域名,需指定为 IP</value>
<value>回退 DNS 解析,需指定为 IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray freedom 解析策略</value>
@ -1440,6 +1440,9 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>添加常用 DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>开启后可覆盖 sing-box DoH 解析服务器</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>

View file

@ -1426,7 +1426,7 @@
<value>Bootstrap DNS (sing-box)</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Resolve DNS server domains, requires IP</value>
<value>Fallback DNS Resolution, Require IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value>
@ -1440,6 +1440,9 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>The sing-box DoH resolution server can be overwritten</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>

View file

@ -1,5 +1,3 @@
using DynamicData;
namespace ServiceLib.Services;
/// <summary>
@ -126,7 +124,7 @@ public class ActionPrecheckService(Config config)
return errors;
}
var hasCycle = ProfileGroupItemManager.HasCycle(item.IndexId);
var hasCycle = await item.HasCycle(new HashSet<string>(), new HashSet<string>());
if (hasCycle)
{
errors.Add(string.Format(ResUI.GroupSelfReference, item.Remarks));

View file

@ -363,6 +363,8 @@ public partial class CoreConfigSingboxService(Config config)
await GenLog(singboxConfig);
await GenInbounds(singboxConfig);
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
var groupRet = await GenGroupOutbound(parentNode, singboxConfig);
if (groupRet != 0)
@ -371,8 +373,6 @@ public partial class CoreConfigSingboxService(Config config)
return ret;
}
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
await GenDns(null, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);
@ -416,10 +416,12 @@ public partial class CoreConfigSingboxService(Config config)
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
singboxConfig.outbounds.RemoveAt(0);
await GenLog(singboxConfig);
await GenInbounds(singboxConfig);
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
singboxConfig.outbounds.RemoveAt(0);
var groupRet = await GenGroupOutbound(parentNode, singboxConfig);
if (groupRet != 0)
@ -428,8 +430,6 @@ public partial class CoreConfigSingboxService(Config config)
return ret;
}
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
await GenDns(null, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);

View file

@ -212,13 +212,33 @@ public partial class CoreConfigSingboxService
{
return -1;
}
var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem);
if (profileGroupItem is null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return -1;
}
var hasCycle = await node.HasCycle(new HashSet<string>(), new HashSet<string>());
if (hasCycle)
{
return -1;
}
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
// 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;
@ -494,7 +514,16 @@ public partial class CoreConfigSingboxService
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
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;
@ -611,12 +640,12 @@ public partial class CoreConfigSingboxService
}
// Merge results: first the selector/urltest/proxies, then other outbounds, and finally prev outbounds
var serverList = new List<BaseServer4Sbox>();
serverList = serverList.Concat(prevOutbounds)
.Concat(resultOutbounds)
.Concat(resultEndpoints)
.ToList();
await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(singboxConfig.outbounds);
singboxConfig.outbounds = resultOutbounds;
singboxConfig.endpoints ??= new List<Endpoints4Sbox>();
resultEndpoints.AddRange(singboxConfig.endpoints);
singboxConfig.endpoints = resultEndpoints;
}
catch (Exception ex)
{
@ -665,30 +694,6 @@ public partial class CoreConfigSingboxService
for (var i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
if (node == null)
continue;
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
continue;
}
var childBaseTagName = $"{baseTagName}-{i + 1}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
if (ret == 0)
{
proxyTags.Add(childBaseTagName);
}
continue;
}
var server = await GenServer(node);
if (server is null)
{
@ -732,11 +737,12 @@ public partial class CoreConfigSingboxService
resultOutbounds.Insert(0, outUrltest);
resultOutbounds.Insert(0, outSelector);
}
var serverList = new List<BaseServer4Sbox>();
serverList = serverList.Concat(resultOutbounds)
.Concat(resultEndpoints)
.ToList();
await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
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);
}
@ -779,40 +785,14 @@ public partial class CoreConfigSingboxService
resultOutbounds.Add(outbound);
}
}
var serverList = new List<BaseServer4Sbox>();
serverList = serverList.Concat(resultOutbounds)
.Concat(resultEndpoints)
.ToList();
await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
return await Task.FromResult(0);
}
singboxConfig.outbounds ??= new();
resultOutbounds.AddRange(singboxConfig.outbounds);
singboxConfig.outbounds = resultOutbounds;
singboxConfig.endpoints ??= new();
resultEndpoints.AddRange(singboxConfig.endpoints);
singboxConfig.endpoints = resultEndpoints;
private async Task<int> AddRangeOutbounds(List<BaseServer4Sbox> servers, SingboxConfig singboxConfig, bool prepend = true)
{
try
{
if (servers is null || servers.Count <= 0)
{
return 0;
}
var outbounds = servers.Where(s => s is Outbound4Sbox).Cast<Outbound4Sbox>().ToList();
var endpoints = servers.Where(s => s is Endpoints4Sbox).Cast<Endpoints4Sbox>().ToList();
singboxConfig.endpoints ??= new();
if (prepend)
{
singboxConfig.outbounds.InsertRange(0, outbounds);
singboxConfig.endpoints.InsertRange(0, endpoints);
}
else
{
singboxConfig.outbounds.AddRange(outbounds);
singboxConfig.endpoints.AddRange(endpoints);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
}

View file

@ -376,7 +376,7 @@ public partial class CoreConfigSingboxService
return Global.ProxyTag;
}
var tag = $"{node.IndexId}-{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)))
{
@ -385,10 +385,11 @@ public partial class CoreConfigSingboxService
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
var ret = await GenGroupOutbound(node, singboxConfig, tag);
var childBaseTagName = $"{Global.ProxyTag}-{node.IndexId}";
var ret = await GenGroupOutbound(node, singboxConfig, childBaseTagName);
if (ret == 0)
{
return tag;
return childBaseTagName;
}
return Global.ProxyTag;
}

View file

@ -114,6 +114,9 @@ public partial class CoreConfigV2rayService(Config config)
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)
@ -122,15 +125,11 @@ public partial class CoreConfigV2rayService(Config config)
return ret;
}
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
var defaultBalancerTag = $"{Global.ProxyTag}{Global.BalancerTagSuffix}";
//add rule
var rules = v2rayConfig.routing.rules;
if (rules?.Count > 0 && ((v2rayConfig.routing.balancers?.Count ?? 0) > 0))
if (rules?.Count > 0)
{
var balancerTagSet = v2rayConfig.routing.balancers
.Select(b => b.tag)
@ -177,7 +176,7 @@ public partial class CoreConfigV2rayService(Config config)
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
ret.Data = await ApplyFullConfigTemplate(v2rayConfig, true);
return ret;
}
catch (Exception ex)
@ -216,10 +215,13 @@ public partial class CoreConfigV2rayService(Config config)
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
v2rayConfig.outbounds.RemoveAt(0);
await GenLog(v2rayConfig);
await GenInbounds(v2rayConfig);
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
v2rayConfig.outbounds.RemoveAt(0);
var groupRet = await GenGroupOutbound(parentNode, v2rayConfig);
if (groupRet != 0)
@ -228,13 +230,9 @@ public partial class CoreConfigV2rayService(Config config)
return ret;
}
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
ret.Data = await ApplyFullConfigTemplate(v2rayConfig, true);
return ret;
}
catch (Exception ex)

View file

@ -4,80 +4,32 @@ public partial class CoreConfigV2rayService
{
private async Task<int> GenObservatory(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag)
{
// Collect all existing subject selectors from both observatories
var subjectSelectors = new List<string>();
subjectSelectors.AddRange(v2rayConfig.burstObservatory?.subjectSelector ?? []);
subjectSelectors.AddRange(v2rayConfig.observatory?.subjectSelector ?? []);
// Case 1: exact match already exists -> nothing to do
if (subjectSelectors.Any(baseTagName.StartsWith))
return await Task.FromResult(0);
// Case 2: prefix match exists -> reuse it and move to the first position
var matched = subjectSelectors.FirstOrDefault(s => s.StartsWith(baseTagName));
if (matched is not null)
if (multipleLoad == EMultipleLoad.LeastPing)
{
baseTagName = matched;
if (v2rayConfig.burstObservatory?.subjectSelector?.Contains(baseTagName) == true)
var observatory = new Observatory4Ray
{
v2rayConfig.burstObservatory.subjectSelector.Remove(baseTagName);
v2rayConfig.burstObservatory.subjectSelector.Insert(0, baseTagName);
}
if (v2rayConfig.observatory?.subjectSelector?.Contains(baseTagName) == true)
{
v2rayConfig.observatory.subjectSelector.Remove(baseTagName);
v2rayConfig.observatory.subjectSelector.Insert(0, baseTagName);
}
return await Task.FromResult(0);
subjectSelector = [baseTagName],
probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
probeInterval = "3m",
enableConcurrency = true,
};
v2rayConfig.observatory = observatory;
}
// Case 3: need to create or insert based on multipleLoad type
if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback)
else if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback)
{
if (v2rayConfig.burstObservatory is null)
var burstObservatory = new BurstObservatory4Ray
{
// Create new burst observatory with default ping config
v2rayConfig.burstObservatory = new BurstObservatory4Ray
subjectSelector = [baseTagName],
pingConfig = new()
{
subjectSelector = [baseTagName],
pingConfig = new()
{
destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
interval = "5m",
timeout = "30s",
sampling = 2,
}
};
}
else
{
v2rayConfig.burstObservatory.subjectSelector ??= new();
v2rayConfig.burstObservatory.subjectSelector.Add(baseTagName);
}
destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
interval = "5m",
timeout = "30s",
sampling = 2,
}
};
v2rayConfig.burstObservatory = burstObservatory;
}
else if (multipleLoad is EMultipleLoad.LeastPing)
{
if (v2rayConfig.observatory is null)
{
// Create new observatory with default probe config
v2rayConfig.observatory = new Observatory4Ray
{
subjectSelector = [baseTagName],
probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
probeInterval = "3m",
enableConcurrency = true,
};
}
else
{
v2rayConfig.observatory.subjectSelector ??= new();
v2rayConfig.observatory.subjectSelector.Add(baseTagName);
}
}
return await Task.FromResult(0);
}

View file

@ -4,7 +4,7 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService
{
private async Task<string> ApplyFullConfigTemplate(V2rayConfig v2rayConfig)
private async Task<string> ApplyFullConfigTemplate(V2rayConfig v2rayConfig, bool handleBalancerAndRules = false)
{
var fullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray);
if (fullConfigTemplate == null || !fullConfigTemplate.Enabled || fullConfigTemplate.Config.IsNullOrEmpty())
@ -19,7 +19,7 @@ public partial class CoreConfigV2rayService
}
// Handle balancer and rules modifications (for multiple load scenarios)
if (v2rayConfig.routing?.balancers?.Count > 0)
if (handleBalancerAndRules && v2rayConfig.routing?.balancers?.Count > 0)
{
var balancer = v2rayConfig.routing.balancers.First();
@ -60,34 +60,6 @@ public partial class CoreConfigV2rayService
}
}
if (v2rayConfig.observatory != null)
{
if (fullConfigTemplateNode["observatory"] == null)
{
fullConfigTemplateNode["observatory"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.observatory));
}
else
{
var subjectSelector = v2rayConfig.observatory.subjectSelector;
subjectSelector.AddRange(fullConfigTemplateNode["observatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
fullConfigTemplateNode["observatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList()));
}
}
if (v2rayConfig.burstObservatory != null)
{
if (fullConfigTemplateNode["burstObservatory"] == null)
{
fullConfigTemplateNode["burstObservatory"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.burstObservatory));
}
else
{
var subjectSelector = v2rayConfig.burstObservatory.subjectSelector;
subjectSelector.AddRange(fullConfigTemplateNode["burstObservatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
fullConfigTemplateNode["burstObservatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList()));
}
}
// Handle outbounds - append instead of override
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray();
foreach (var outbound in v2rayConfig.outbounds)

View file

@ -488,13 +488,33 @@ public partial class CoreConfigV2rayService
{
return -1;
}
var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem);
if (profileGroupItem is null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return -1;
}
var hasCycle = await node.HasCycle(new HashSet<string>(), new HashSet<string>());
if (hasCycle)
{
return -1;
}
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
// 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;
@ -519,11 +539,9 @@ public partial class CoreConfigV2rayService
}
//add balancers
if (node.ConfigType == EConfigType.PolicyGroup)
{
await GenObservatory(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName);
await GenBalancer(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName);
}
await GenObservatory(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName);
await GenBalancer(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName);
}
catch (Exception ex)
{
@ -631,7 +649,16 @@ public partial class CoreConfigV2rayService
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
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;
@ -699,17 +726,9 @@ public partial class CoreConfigV2rayService
}
// Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds
if (baseTagName == Global.ProxyTag)
{
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(prevOutbounds);
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
catch (Exception ex)
{
@ -778,26 +797,6 @@ public partial class CoreConfigV2rayService
for (var i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
if (node == null)
continue;
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
continue;
}
var childBaseTagName = $"{baseTagName}-{i + 1}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsListWithChain(childProfiles, v2rayConfig, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
continue;
}
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (txtOutbound.IsNullOrEmpty())
{
@ -812,19 +811,13 @@ public partial class CoreConfigV2rayService
outbound.tag = baseTagName + (i + 1).ToString();
resultOutbounds.Add(outbound);
}
if (baseTagName == Global.ProxyTag)
{
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
v2rayConfig.outbounds ??= new();
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
return await Task.FromResult(0);
}
private async Task<int> GenChainOutboundsList(List<ProfileItem> nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag)
private async Task<int> GenChainOutboundsList(List<ProfileItem> nodes, V2rayConfig v2RayConfig, string baseTagName = Global.ProxyTag)
{
// Based on actual network flow instead of data packets
var nodesReverse = nodes.AsEnumerable().Reverse().ToList();
@ -865,15 +858,9 @@ public partial class CoreConfigV2rayService
resultOutbounds.Add(outbound);
}
if (baseTagName == Global.ProxyTag)
{
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
v2RayConfig.outbounds ??= new();
resultOutbounds.AddRange(v2RayConfig.outbounds);
v2RayConfig.outbounds = resultOutbounds;
return await Task.FromResult(0);
}

View file

@ -133,7 +133,7 @@ public partial class CoreConfigV2rayService
return Global.ProxyTag;
}
var tag = $"{node.IndexId}-{Global.ProxyTag}";
var tag = Global.ProxyTag + node.IndexId.ToString();
if (v2rayConfig.outbounds.Any(p => p.tag == tag))
{
return tag;
@ -141,10 +141,11 @@ public partial class CoreConfigV2rayService
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{
var ret = await GenGroupOutbound(node, v2rayConfig, tag);
var childBaseTagName = $"{Global.ProxyTag}-{node.IndexId}";
var ret = await GenGroupOutbound(node, v2rayConfig, childBaseTagName);
if (ret == 0)
{
return tag;
return childBaseTagName;
}
return Global.ProxyTag;
}

View file

@ -195,12 +195,12 @@ public class AddGroupServerViewModel : MyReactiveObject
var childIndexIds = new List<string>();
foreach (var item in ChildItemsObs)
{
if (item.IndexId.IsNullOrEmpty())
if (!item.IndexId.IsNullOrEmpty())
{
continue;
childIndexIds.Add(item.IndexId);
}
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

View file

@ -20,6 +20,7 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="Assets/GlobalResources.axaml" />
<ResourceInclude Source="Controls/AutoCompleteBox.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>

View file

@ -1,7 +1,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
@ -19,7 +18,7 @@ internal class AvaUtils
return null;
}
return await clipboard.TryGetTextAsync();
return await clipboard.GetTextAsync();
}
catch
{
@ -34,7 +33,9 @@ internal class AvaUtils
var clipboard = TopLevel.GetTopLevel(visual)?.Clipboard;
if (clipboard == null)
return;
await clipboard.SetTextAsync(strData);
var dataObject = new DataObject();
dataObject.Set(DataFormats.Text, strData);
await clipboard.SetDataObjectAsync(dataObject);
}
catch
{

View file

@ -0,0 +1,48 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:v2rayN.Desktop.Controls">
<!-- Add Resources Here -->
<ControlTheme x:Key="{x:Type controls:AutoCompleteBox}" TargetType="controls:AutoCompleteBox">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="MinHeight" Value="{DynamicResource AutoCompleteBoxDefaultHeight}" />
<Setter Property="MaxDropDownHeight" Value="{DynamicResource AutoCompleteMaxDropdownHeight}" />
<Setter Property="Template">
<ControlTemplate TargetType="AutoCompleteBox">
<Panel>
<TextBox
Name="PART_TextBox"
MinHeight="{TemplateBinding MinHeight}"
VerticalAlignment="Stretch"
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
InnerLeftContent="{TemplateBinding InnerLeftContent}"
InnerRightContent="{TemplateBinding InnerRightContent}"
Watermark="{TemplateBinding Watermark}" />
<Popup
Name="PART_Popup"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
IsLightDismissEnabled="True"
PlacementTarget="{TemplateBinding}">
<Border
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
Margin="{DynamicResource AutoCompleteBoxPopupMargin}"
Padding="{DynamicResource AutoCompleteBoxPopupPadding}"
HorizontalAlignment="Stretch"
Background="{DynamicResource AutoCompleteBoxPopupBackground}"
BorderBrush="{DynamicResource AutoCompleteBoxPopupBorderBrush}"
BorderThickness="{DynamicResource AutoCompleteBoxPopupBorderThickness}"
BoxShadow="{DynamicResource AutoCompleteBoxPopupBoxShadow}"
CornerRadius="{DynamicResource AutoCompleteBoxPopupCornerRadius}">
<ListBox
Name="PART_SelectingItemsControl"
Foreground="{TemplateBinding Foreground}"
ItemTemplate="{TemplateBinding ItemTemplate}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
</Border>
</Popup>
</Panel>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

View file

@ -0,0 +1,40 @@
using Avalonia.Input;
using Avalonia.Interactivity;
namespace v2rayN.Desktop.Controls;
public class AutoCompleteBox : Avalonia.Controls.AutoCompleteBox
{
static AutoCompleteBox()
{
MinimumPrefixLengthProperty.OverrideDefaultValue<AutoCompleteBox>(0);
}
public AutoCompleteBox()
{
AddHandler(PointerPressedEvent, OnBoxPointerPressed, RoutingStrategies.Tunnel);
}
private void OnBoxPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (Equals(sender, this) && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
SetCurrentValue(IsDropDownOpenProperty, true);
}
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
if (IsDropDownOpen)
{
return;
}
SetCurrentValue(IsDropDownOpenProperty, true);
}
public void Clear()
{
SetCurrentValue(SelectedItemProperty, null);
}
}

View file

@ -2,6 +2,7 @@
x:Class="v2rayN.Desktop.Views.DNSSettingWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrls="clr-namespace:v2rayN.Desktop.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
@ -55,13 +56,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbDomesticDNS}" />
<ComboBox
<ctrls:AutoCompleteBox
x:Name="cmbDirectDNS"
Grid.Row="1"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Text="{Binding DirectDNS, Mode=TwoWay}" />
<TextBlock
Grid.Row="2"
@ -69,13 +70,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRemoteDNS}" />
<ComboBox
<ctrls:AutoCompleteBox
x:Name="cmbRemoteDNS"
Grid.Row="2"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Text="{Binding RemoteDNS, Mode=TwoWay}" />
<TextBlock
Grid.Row="3"
@ -83,13 +84,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSBOutboundsResolverDNS}" />
<ComboBox
<ctrls:AutoCompleteBox
x:Name="cmbSBResolverDNS"
Grid.Row="3"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Text="{Binding SingboxOutboundsResolveDNS, Mode=TwoWay}" />
<TextBlock
Grid.Row="3"
Grid.Column="2"
@ -104,13 +105,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSBBootstrapDNS}" />
<ComboBox
<ctrls:AutoCompleteBox
x:Name="cmbSBFinalResolverDNS"
Grid.Row="4"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Text="{Binding SingboxFinalResolveDNS, Mode=TwoWay}" />
<TextBlock
Grid.Row="4"
Grid.Column="2"
@ -173,6 +174,13 @@
Grid.Column="1"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="8"
Grid.Column="2"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSBDoHOverride}"
TextWrapping="Wrap" />
</Grid>
</ScrollViewer>
</TabItem>
@ -252,13 +260,13 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbValidateDirectExpectedIPs}" />
<ComboBox
<ctrls:AutoCompleteBox
x:Name="cmbDirectExpectedIPs"
Grid.Row="4"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Text="{Binding DirectExpectedIPs, Mode=TwoWay}" />
<TextBlock
Grid.Row="4"
Grid.Column="2"
@ -355,11 +363,11 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsDomainDNSAddress}" />
<ComboBox
<ctrls:AutoCompleteBox
x:Name="cmbdomainDNSAddressCompatible"
Width="150"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Text="{Binding DomainDNSAddressCompatible, Mode=TwoWay}" />
</StackPanel>
</WrapPanel>
@ -427,11 +435,11 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsDomainDNSAddress}" />
<ComboBox
<ctrls:AutoCompleteBox
x:Name="cmbdomainDNSAddress2Compatible"
Width="150"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Text="{Binding DomainDNSAddress2Compatible, Mode=TwoWay}" />
</StackPanel>
</WrapPanel>

View file

@ -40,15 +40,15 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
this.Bind(ViewModel, vm => vm.AddCommonHosts, v => v.togAddCommonHosts.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.FakeIP, v => v.togFakeIP.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.BlockBindingQuery, v => v.togBlockBindingQuery.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxOutboundsResolveDNS, v => v.cmbSBResolverDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxFinalResolveDNS, v => v.cmbSBFinalResolverDNS.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.DirectDNS, v => v.cmbDirectDNS.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.RemoteDNS, v => v.cmbRemoteDNS.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.SingboxOutboundsResolveDNS, v => v.cmbSBResolverDNS.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.SingboxFinalResolveDNS, v => v.cmbSBFinalResolverDNS.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RayStrategy4Freedom, v => v.cmbRayFreedomDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxStrategy4Direct, v => v.cmbSBDirectDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SingboxStrategy4Proxy, v => v.cmbSBRemoteDNSStrategy.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Hosts, v => v.txtHosts.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.Text).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
@ -57,11 +57,11 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
this.Bind(ViewModel, vm => vm.UseSystemHostsCompatible, v => v.togUseSystemHostsCompatible.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainStrategy4FreedomCompatible, v => v.cmbdomainStrategy4FreedomCompatible.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainDNSAddressCompatible, v => v.cmbdomainDNSAddressCompatible.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.DomainDNSAddressCompatible, v => v.cmbdomainDNSAddressCompatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NormalDNSCompatible, v => v.txtnormalDNSCompatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainStrategy4Freedom2Compatible, v => v.cmbdomainStrategy4OutCompatible.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DomainDNSAddress2Compatible, v => v.cmbdomainDNSAddress2Compatible.Text).DisposeWith(disposables);
//this.Bind(ViewModel, vm => vm.DomainDNSAddress2Compatible, v => v.cmbdomainDNSAddress2Compatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.NormalDNS2Compatible, v => v.txtnormalDNS2Compatible.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunDNS2Compatible, v => v.txttunDNS2Compatible.Text).DisposeWith(disposables);

View file

@ -2,6 +2,7 @@
x:Class="v2rayN.Desktop.Views.OptionSettingWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrls="clr-namespace:v2rayN.Desktop.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
@ -501,13 +502,12 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsCurrentFontFamily}" />
<ComboBox
<ctrls:AutoCompleteBox
x:Name="cmbcurrentFontFamily"
Grid.Row="15"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="15"
Grid.Column="2"
@ -548,13 +548,12 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsSpeedTestUrl}" />
<ComboBox
<ctrls:AutoCompleteBox
Name="cmbSpeedTestUrl"
Grid.Row="18"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="19"
@ -562,13 +561,12 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsSpeedPingTestUrl}" />
<ComboBox
<ctrls:AutoCompleteBox
x:Name="cmbSpeedPingTestUrl"
Grid.Row="19"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="20"
@ -589,13 +587,12 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSettingsSubConvert}" />
<ComboBox
<ctrls:AutoCompleteBox
x:Name="cmbSubConvertUrl"
Grid.Row="21"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="22"
@ -621,8 +618,7 @@
Grid.Row="23"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="23"
Grid.Column="2"
@ -642,8 +638,7 @@
Grid.Row="24"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="24"
Grid.Column="2"
@ -663,8 +658,7 @@
Grid.Row="25"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Margin="{StaticResource Margin4}" />
<TextBlock
Grid.Row="25"
Grid.Column="2"

View file

@ -2,6 +2,7 @@
x:Class="v2rayN.Desktop.Views.RoutingRuleDetailsWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrls="clr-namespace:v2rayN.Desktop.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
@ -46,26 +47,28 @@
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="outboundTag" />
<ComboBox
<ctrls:AutoCompleteBox
Name="cmbOutboundTag"
Grid.Row="1"
Grid.Column="1"
Width="300"
Margin="{StaticResource Margin4}"
IsEditable="True" />
Text="{Binding SelectedSource.OutboundTag, Mode=TwoWay}" />
<StackPanel
Grid.Row="1"
Grid.Column="2"
Margin="{StaticResource Margin4}"
Orientation="Horizontal"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Orientation="Horizontal">
VerticalAlignment="Center">
<Button
x:Name="btnSelectProfile"
Margin="0,0,8,0"
Click="BtnSelectProfile_Click"
Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock VerticalAlignment="Center" Text="{x:Static resx:ResUI.TbRuleOutboundTagTip}" />
Content="{x:Static resx:ResUI.TbSelectProfile}"
Click="BtnSelectProfile_Click" />
<TextBlock
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbRuleOutboundTagTip}" />
</StackPanel>
<TextBlock

View file

@ -43,7 +43,6 @@ public partial class RoutingRuleDetailsWindow : WindowBase<RoutingRuleDetailsVie
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.SelectedSource.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.OutboundTag, v => v.cmbOutboundTag.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Port, v => v.txtPort.Text).DisposeWith(disposables);

View file

@ -210,6 +210,13 @@
Grid.Column="1"
Margin="{StaticResource Margin8}"
HorizontalAlignment="Left" />
<TextBlock
Grid.Row="8"
Grid.Column="3"
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSBDoHOverride}" />
</Grid>
</ScrollViewer>
</TabItem>