Code clean
Some checks failed
release Linux / build (push) Has been cancelled
release Linux / build and release deb x64 & arm64 (push) Has been cancelled
release Linux / build and release rpm x64 & arm64 (push) Has been cancelled
release Linux / build and release rpm riscv64 (push) Has been cancelled
release macOS / build (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (push) Has been cancelled
release Windows / build (push) Has been cancelled
release Linux / release-zip (push) Has been cancelled
release macOS / release-zip (push) Has been cancelled
release macOS / package and release macOS dmg (push) Has been cancelled
release Windows desktop (Avalonia UI) / release-zip (push) Has been cancelled
release Windows / release-zip (push) Has been cancelled

This commit is contained in:
2dust 2026-04-26 19:24:57 +08:00
parent ae662a628d
commit 05e349e45c
18 changed files with 148 additions and 99 deletions

View file

@ -9,105 +9,105 @@ namespace ServiceLib.Tests.CoreConfig.Context;
public class CoreConfigContextBuilderTests
{
[Fact]
public async Task ResolveNodeAsync_DirectCycleDependency_ShouldFailWithCycleError()
{
var config = CoreConfigTestFactory.CreateConfig();
CoreConfigTestFactory.BindAppManagerConfig(config);
[Fact]
public async Task ResolveNodeAsync_DirectCycleDependency_ShouldFailWithCycleError()
{
var config = CoreConfigTestFactory.CreateConfig();
CoreConfigTestFactory.BindAppManagerConfig(config);
var groupAId = NewId("group-a");
var groupBId = NewId("group-b");
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId]);
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupAId]);
var groupAId = NewId("group-a");
var groupBId = NewId("group-b");
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId]);
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupAId]);
await UpsertProfilesAsync(groupA, groupB);
await UpsertProfilesAsync(groupA, groupB);
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
context.AllProxiesMap.Clear();
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
context.AllProxiesMap.Clear();
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
validatorResult.Success.Should().BeFalse();
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
}
validatorResult.Success.Should().BeFalse();
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
}
[Fact]
public async Task ResolveNodeAsync_IndirectCycleDependency_ShouldFailWithCycleError()
{
var config = CoreConfigTestFactory.CreateConfig();
CoreConfigTestFactory.BindAppManagerConfig(config);
[Fact]
public async Task ResolveNodeAsync_IndirectCycleDependency_ShouldFailWithCycleError()
{
var config = CoreConfigTestFactory.CreateConfig();
CoreConfigTestFactory.BindAppManagerConfig(config);
var groupAId = NewId("group-a");
var groupBId = NewId("group-b");
var groupCId = NewId("group-c");
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId]);
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupCId]);
var groupC = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupCId, "group-c", [groupAId]);
var groupAId = NewId("group-a");
var groupBId = NewId("group-b");
var groupCId = NewId("group-c");
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId]);
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupCId]);
var groupC = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupCId, "group-c", [groupAId]);
await UpsertProfilesAsync(groupA, groupB, groupC);
await UpsertProfilesAsync(groupA, groupB, groupC);
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
context.AllProxiesMap.Clear();
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
context.AllProxiesMap.Clear();
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
validatorResult.Success.Should().BeFalse();
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
context.AllProxiesMap.Should().NotContainKey(groupC.IndexId);
}
validatorResult.Success.Should().BeFalse();
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
context.AllProxiesMap.Should().NotContainKey(groupC.IndexId);
}
[Fact]
public async Task ResolveNodeAsync_CycleWithValidBranch_ShouldSkipCycleAndKeepValidChild()
{
var config = CoreConfigTestFactory.CreateConfig();
CoreConfigTestFactory.BindAppManagerConfig(config);
[Fact]
public async Task ResolveNodeAsync_CycleWithValidBranch_ShouldSkipCycleAndKeepValidChild()
{
var config = CoreConfigTestFactory.CreateConfig();
CoreConfigTestFactory.BindAppManagerConfig(config);
var groupAId = NewId("group-a");
var groupBId = NewId("group-b");
var leafId = NewId("leaf");
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId, leafId]);
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupAId]);
var leaf = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, leafId, "leaf");
var groupAId = NewId("group-a");
var groupBId = NewId("group-b");
var leafId = NewId("leaf");
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId, leafId]);
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupAId]);
var leaf = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, leafId, "leaf");
await UpsertProfilesAsync(groupA, groupB, leaf);
await UpsertProfilesAsync(groupA, groupB, leaf);
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
context.AllProxiesMap.Clear();
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
context.AllProxiesMap.Clear();
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
validatorResult.Success.Should().BeTrue();
validatorResult.Errors.Should().BeEmpty();
validatorResult.Warnings.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
validatorResult.Success.Should().BeTrue();
validatorResult.Errors.Should().BeEmpty();
validatorResult.Warnings.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
context.AllProxiesMap.Should().ContainKey(leaf.IndexId);
context.AllProxiesMap.Should().ContainKey(groupA.IndexId);
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
groupA.GetProtocolExtra().ChildItems.Should().Be(leaf.IndexId);
}
context.AllProxiesMap.Should().ContainKey(leaf.IndexId);
context.AllProxiesMap.Should().ContainKey(groupA.IndexId);
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
groupA.GetProtocolExtra().ChildItems.Should().Be(leaf.IndexId);
}
private static string NewId(string prefix)
{
return $"{prefix}-{Guid.NewGuid():N}";
}
private static string NewId(string prefix)
{
return $"{prefix}-{Guid.NewGuid():N}";
}
private static bool ContainsCycleDependencyMessage(string message)
{
return message.Contains("cycle dependency", StringComparison.OrdinalIgnoreCase)
|| message.Contains("循环依赖", StringComparison.Ordinal)
|| message.Contains("循環依賴", StringComparison.Ordinal);
}
private static bool ContainsCycleDependencyMessage(string message)
{
return message.Contains("cycle dependency", StringComparison.OrdinalIgnoreCase)
|| message.Contains("循环依赖", StringComparison.Ordinal)
|| message.Contains("循環依賴", StringComparison.Ordinal);
}
private static async Task UpsertProfilesAsync(params ProfileItem[] profiles)
{
SQLiteHelper.Instance.CreateTable<ProfileItem>();
foreach (var profile in profiles)
{
await SQLiteHelper.Instance.ReplaceAsync(profile);
}
}
private static async Task UpsertProfilesAsync(params ProfileItem[] profiles)
{
SQLiteHelper.Instance.CreateTable<ProfileItem>();
foreach (var profile in profiles)
{
await SQLiteHelper.Instance.ReplaceAsync(profile);
}
}
}

View file

@ -1,7 +1,7 @@
using System.Reflection;
using ServiceLib.Enums;
using ServiceLib.Manager;
using ServiceLib.Models;
using System.Reflection;
namespace ServiceLib.Tests.CoreConfig;
@ -33,7 +33,10 @@ internal static class CoreConfigTestFactory
UiItem =
new UIItem
{
CurrentLanguage = "en", CurrentFontFamily = "sans", MainColumnItem = [], WindowSizeItem = []
CurrentLanguage = "en",
CurrentFontFamily = "sans",
MainColumnItem = [],
WindowSizeItem = []
},
ConstItem = new ConstItem(),
SpeedTestItem = new SpeedTestItem
@ -51,7 +54,8 @@ internal static class CoreConfigTestFactory
SystemProxyItem =
new SystemProxyItem
{
SystemProxyExceptions = string.Empty, SystemProxyAdvancedProtocol = string.Empty
SystemProxyExceptions = string.Empty,
SystemProxyAdvancedProtocol = string.Empty
},
WebDavItem = new WebDavItem(),
CheckUpdateItem = new CheckUpdateItem(),
@ -131,11 +135,15 @@ internal static class CoreConfigTestFactory
{
var node = new ProfileItem
{
IndexId = indexId, ConfigType = EConfigType.PolicyGroup, CoreType = coreType, Remarks = remarks,
IndexId = indexId,
ConfigType = EConfigType.PolicyGroup,
CoreType = coreType,
Remarks = remarks,
};
node.SetProtocolExtra(node.GetProtocolExtra() with
{
GroupType = nameof(EConfigType.PolicyGroup), ChildItems = string.Join(",", childIndexIds),
GroupType = nameof(EConfigType.PolicyGroup),
ChildItems = string.Join(",", childIndexIds),
});
return node;
@ -146,11 +154,15 @@ internal static class CoreConfigTestFactory
{
var node = new ProfileItem
{
IndexId = indexId, ConfigType = EConfigType.ProxyChain, CoreType = coreType, Remarks = remarks,
IndexId = indexId,
ConfigType = EConfigType.ProxyChain,
CoreType = coreType,
Remarks = remarks,
};
node.SetProtocolExtra(node.GetProtocolExtra() with
{
GroupType = nameof(EConfigType.ProxyChain), ChildItems = string.Join(",", childIndexIds),
GroupType = nameof(EConfigType.ProxyChain),
ChildItems = string.Join(",", childIndexIds),
});
return node;

View file

@ -1,7 +1,7 @@
using AwesomeAssertions;
using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Models;
using ServiceLib.Enums;
using Xunit;
namespace ServiceLib.Tests.Fmt;
@ -92,7 +92,7 @@ public class FmtHandlerTests
var uri = FmtHandler.GetShareUri(source);
uri.Should().NotBeNullOrWhiteSpace();
(uri!.StartsWith(Global.ProtocolShares[source.ConfigType], StringComparison.OrdinalIgnoreCase)).Should()
uri!.StartsWith(Global.ProtocolShares[source.ConfigType], StringComparison.OrdinalIgnoreCase).Should()
.BeTrue();
var resolved = FmtHandler.ResolveConfig(uri, out var msg);

View file

@ -239,7 +239,9 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
var clientAddrForSocks = new Socks5AddressData
{
AddressType = Socks5AddressData.AddrTypeIPv4, Host = "0.0.0.0", Port = 0
AddressType = Socks5AddressData.AddrTypeIPv4,
Host = "0.0.0.0",
Port = 0
};
using var udpAssociateReqMs = new MemoryStream();
udpAssociateReqMs.WriteByte(Socks5Version);
@ -267,7 +269,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
return true;
}
#endregion
#endregion SOCKS5 Connection Handling
#region SOCKS5 Address Handling
@ -298,6 +300,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
}
break;
case AddrTypeDomain:
if (string.IsNullOrEmpty(Host))
{
@ -311,6 +314,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
}
break;
case AddrTypeIPv6:
if (IPAddress.TryParse(Host, out var ip6) && ip6.AddressFamily == AddressFamily.InterNetworkV6)
{
@ -322,6 +326,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
}
break;
default:
throw new NotSupportedException($"SOCKS5 address type {AddressType} not supported.");
}
@ -355,6 +360,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
addr.Host = new IPAddress(ipv4Bytes).ToString();
break;
case AddrTypeDomain:
var lenByte = new byte[1];
if (await stream.ReadAsync(lenByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1)
@ -379,6 +385,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
}
break;
case AddrTypeIPv6:
var ipv6Bytes = new byte[16];
if (await stream.ReadAsync(ipv6Bytes.AsMemory(0, 16), ct).ConfigureAwait(false) < 16)
@ -388,6 +395,7 @@ public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposabl
addr.Host = new IPAddress(ipv6Bytes).ToString();
break;
default:
return null;
}

View file

@ -4,6 +4,7 @@ public class DnsService : IUdpTest
{
private const int DnsDefaultPort = 53;
private const string DnsDefaultServer = "8.8.8.8"; // Google Public DNS
private static readonly byte[] DnsQueryPacket =
[
// Header: ID=0x1234, Standard query with RD set, QDCOUNT=1

View file

@ -3,7 +3,10 @@ namespace ServiceLib.UdpTest.Tester;
public interface IUdpTest
{
public byte[] BuildUdpRequestPacket();
public bool VerifyAndExtractUdpResponse(byte[] udpResponseBytes);
public ushort GetDefaultTargetPort();
public string GetDefaultTargetHost();
}

View file

@ -4,6 +4,7 @@ public class McBeService : IUdpTest
{
private const int McBeDefaultPort = 19132;
private const string McBeDefaultServer = "pms.mc-complex.com";
// 0x01 | client alive time in ms (unsigned long long) | magic | client GUID
private static readonly byte[] McBeQueryPacket =
[
@ -18,11 +19,13 @@ public class McBeService : IUdpTest
0x66, 0x0E, 0xAB, 0xBC, 0x61, 0x0D, 0x1F, 0x4E,
0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE, 0xF5, 0x4B
];
private static readonly byte[] McBeMagicBytes =
[
0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE,
0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78
];
private static readonly List<string> ValidGameModes =
[
"Survival",
@ -40,9 +43,9 @@ public class McBeService : IUdpTest
{
// 0x1c | client alive time in ms (recorded from previous ping) |
// server GUID | Magic | string length | Edition
//
//
// Edition Example:
//
//
// MCPE;Dedicated Server;527;1.19.1;0;10;13253860892328930865;Bedrock level;Survival;1;19132;19133;
if (mcbeResponseBytes.Length < 48)
{

View file

@ -4,6 +4,7 @@ public class StunService : IUdpTest
{
private const int StunDefaultPort = 3478;
private const string StunDefaultServer = "stun.voztovoice.org";
private static readonly byte[] StunBindingRequestPacket =
[
// STUN Binding Request

View file

@ -5,6 +5,7 @@ namespace ServiceLib.Handler.Fmt;
public class BaseFmt
{
private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure" };
private static string UrlEncodeSafe(string? value) => Utils.UrlEncode(value ?? string.Empty);
protected static string GetIpv6(string address)

View file

@ -237,6 +237,7 @@ public class Transport4Sbox
public class Headers4Sbox
{
public string? Host { get; set; }
[JsonPropertyName("User-Agent")]
public string UserAgent { get; set; }
}

View file

@ -500,12 +500,16 @@ public class MaskSettings4Ray
{
public string? password { get; set; }
public string? domain { get; set; }
// fragment
public string? packets { get; set; }
public string? length { get; set; }
public string? delay { get; set; }
// noise
public int? reset { get; set; }
public List<NoiseMask4Ray>? noise { get; set; }
}
@ -533,6 +537,7 @@ public class AccountsItem4Ray
public class Sockopt4Ray
{
public string? dialerProxy { get; set; }
[JsonPropertyName("interface")]
public string? Interface { get; set; }
}

View file

@ -548,6 +548,7 @@ public partial class CoreConfigV2rayService
FillOutboundMux(outbound);
break;
case nameof(ETransport.grpc):
GrpcSettings4Ray grpcSettings = new()
{

View file

@ -299,7 +299,6 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
return url;
}
else if (Utils.IsLinux())
{
var arch = RuntimeInformation.ProcessArchitecture;
@ -314,7 +313,6 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
_ => null,
};
}
else if (Utils.IsMacOS())
{
return RuntimeInformation.ProcessArchitecture switch

View file

@ -338,21 +338,27 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
case nameof(ETransport.raw):
gridTransportRaw.IsVisible = true;
break;
case nameof(ETransport.kcp):
gridTransportKcp.IsVisible = true;
break;
case nameof(ETransport.ws):
gridTransportWs.IsVisible = true;
break;
case nameof(ETransport.httpupgrade):
gridTransportHttpupgrade.IsVisible = true;
break;
case nameof(ETransport.xhttp):
gridTransportXhttp.IsVisible = true;
break;
case nameof(ETransport.grpc):
gridTransportGrpc.IsVisible = true;
break;
default:
gridTransportRaw.IsVisible = true;
break;

View file

@ -64,7 +64,10 @@ public partial class RoutingSettingWindow : WindowBase<RoutingSettingViewModel>
private void RoutingSettingWindow_Closing(object? sender, WindowClosingEventArgs e)
{
if (_closed) return;
if (_closed)
{
return;
}
// DomainStrategy is auto-saved reactively; just ensure the caller knows changes were made
if (ViewModel?.IsModified == true)

View file

@ -137,7 +137,7 @@ public class ThemeSettingViewModel : MyReactiveObject
private void ModifyFontSize()
{
double size = (long)CurrentFontSize;
double size = CurrentFontSize;
if (size < Global.MinFontSize)
{
return;

View file

@ -340,21 +340,27 @@ public partial class AddServerWindow
case nameof(ETransport.raw):
gridTransportRaw.Visibility = Visibility.Visible;
break;
case nameof(ETransport.kcp):
gridTransportKcp.Visibility = Visibility.Visible;
break;
case nameof(ETransport.ws):
gridTransportWs.Visibility = Visibility.Visible;
break;
case nameof(ETransport.httpupgrade):
gridTransportHttpupgrade.Visibility = Visibility.Visible;
break;
case nameof(ETransport.xhttp):
gridTransportXhttp.Visibility = Visibility.Visible;
break;
case nameof(ETransport.grpc):
gridTransportGrpc.Visibility = Visibility.Visible;
break;
default:
gridTransportRaw.Visibility = Visibility.Visible;
break;

View file

@ -170,10 +170,10 @@ public partial class MainWindow
private void OnProgramStarted(object state, bool timeout)
{
Application.Current?.Dispatcher.Invoke((Action)(() =>
Application.Current?.Dispatcher.Invoke(() =>
{
ShowHideWindow(true);
}));
});
}
private async Task DelegateSnackMsg(string content)