diff --git a/v2rayN/Directory.Build.props b/v2rayN/Directory.Build.props
index 937b6988..9493daa7 100644
--- a/v2rayN/Directory.Build.props
+++ b/v2rayN/Directory.Build.props
@@ -1,7 +1,7 @@
- 7.21.0
+ 7.21.1
diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs
index 5f185a9f..ee329845 100644
--- a/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs
+++ b/v2rayN/ServiceLib.Tests/CoreConfig/Context/CoreConfigContextBuilderTests.cs
@@ -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();
- foreach (var profile in profiles)
- {
- await SQLiteHelper.Instance.ReplaceAsync(profile);
- }
- }
+ private static async Task UpsertProfilesAsync(params ProfileItem[] profiles)
+ {
+ SQLiteHelper.Instance.CreateTable();
+ foreach (var profile in profiles)
+ {
+ await SQLiteHelper.Instance.ReplaceAsync(profile);
+ }
+ }
}
diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs
index c5acd854..812a5a32 100644
--- a/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs
+++ b/v2rayN/ServiceLib.Tests/CoreConfig/CoreConfigTestFactory.cs
@@ -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;
diff --git a/v2rayN/ServiceLib.Tests/Fmt/FmtHandlerTests.cs b/v2rayN/ServiceLib.Tests/Fmt/FmtHandlerTests.cs
index 56fb0f97..143fa3e5 100644
--- a/v2rayN/ServiceLib.Tests/Fmt/FmtHandlerTests.cs
+++ b/v2rayN/ServiceLib.Tests/Fmt/FmtHandlerTests.cs
@@ -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);
diff --git a/v2rayN/ServiceLib.UdpTest/GlobalUsings.cs b/v2rayN/ServiceLib.UdpTest/GlobalUsings.cs
new file mode 100644
index 00000000..2ef96e1b
--- /dev/null
+++ b/v2rayN/ServiceLib.UdpTest/GlobalUsings.cs
@@ -0,0 +1,5 @@
+global using System.Buffers.Binary;
+global using System.Diagnostics;
+global using System.Net;
+global using System.Net.Sockets;
+global using System.Text;
diff --git a/v2rayN/ServiceLib.UdpTest/ServiceLib.UdpTest.csproj b/v2rayN/ServiceLib.UdpTest/ServiceLib.UdpTest.csproj
new file mode 100644
index 00000000..9a69e670
--- /dev/null
+++ b/v2rayN/ServiceLib.UdpTest/ServiceLib.UdpTest.csproj
@@ -0,0 +1,7 @@
+
+
+
+ Library
+
+
+
diff --git a/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs b/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs
new file mode 100644
index 00000000..f9a2951f
--- /dev/null
+++ b/v2rayN/ServiceLib.UdpTest/Socks5UdpChannel.cs
@@ -0,0 +1,420 @@
+namespace ServiceLib.UdpTest;
+
+public class Socks5UdpChannel(string socks5Host, int socks5TcpPort) : IDisposable
+{
+ private TcpClient _tcpClient;
+ private UdpClient _udpClient;
+ private IPEndPoint _relayEndPoint;
+
+ private bool _initialized = false;
+
+ ///
+ /// Send UDP data to a remote endpoint (IP address)
+ ///
+ public async Task SendAsync(IPEndPoint remote, byte[] data)
+ {
+ var addrData = new Socks5AddressData
+ {
+ AddressType = remote.Address.AddressFamily == AddressFamily.InterNetwork
+ ? Socks5AddressData.AddrTypeIPv4
+ : Socks5AddressData.AddrTypeIPv6,
+ Host = remote.Address.ToString(),
+ Port = (ushort)remote.Port
+ };
+ var packet = BuildSocks5UdpPacket(addrData, data);
+ await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
+ }
+
+ ///
+ /// Send UDP data to a remote endpoint (domain name or IP address)
+ ///
+ /// Domain name or IP address
+ /// Port number
+ /// Data to send
+ public async Task SendAsync(string host, ushort port, byte[] data)
+ {
+ var addrData = new Socks5AddressData();
+
+ // Try to parse as IP address first
+ if (IPAddress.TryParse(host, out var ipAddr))
+ {
+ addrData.AddressType = ipAddr.AddressFamily == AddressFamily.InterNetwork
+ ? Socks5AddressData.AddrTypeIPv4
+ : Socks5AddressData.AddrTypeIPv6;
+ addrData.Host = ipAddr.ToString();
+ }
+ else
+ {
+ // Treat as domain name
+ addrData.AddressType = Socks5AddressData.AddrTypeDomain;
+ addrData.Host = host;
+ }
+
+ addrData.Port = port;
+
+ var packet = BuildSocks5UdpPacket(addrData, data);
+ await _udpClient.SendAsync(packet, packet.Length, _relayEndPoint);
+ }
+
+ ///
+ /// Receive UDP data from remote endpoint
+ ///
+ /// Cancellation token to cancel the receive operation
+ /// Remote endpoint information and received data
+ public async Task<(Socks5RemoteEndpoint Remote, byte[] Data)> ReceiveAsync(
+ CancellationToken cancellationToken = default)
+ {
+ var result = await _udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false);
+ var (remote, payload) = ParseSocks5UdpPacket(result.Buffer);
+ return (remote, payload);
+ }
+
+ ///
+ /// Represents a remote endpoint that can be either an IP address or a domain name
+ ///
+ public class Socks5RemoteEndpoint(string host, ushort port, bool isDomain)
+ {
+ public string Host { get; set; } = host;
+ public ushort Port { get; set; } = port;
+ public bool IsDomain { get; set; } = isDomain;
+ }
+
+ private static byte[] BuildSocks5UdpPacket(Socks5AddressData addressData, byte[] data)
+ {
+ using var ms = new MemoryStream();
+
+ // RSV (2 bytes) + FRAG (1 byte) - Reserved and Fragment fields
+ ms.WriteByte(0x00);
+ ms.WriteByte(0x00);
+ ms.WriteByte(0x00);
+
+ // Write address (ATYP + address + port)
+ ms.Write(addressData.ToBytes());
+
+ // User data payload
+ ms.Write(data);
+
+ return ms.ToArray();
+ }
+
+ private static (Socks5RemoteEndpoint Remote, byte[] Data) ParseSocks5UdpPacket(byte[] packet)
+ {
+ if (packet.Length < 10) // Minimum length: RSV(2) + FRAG(1) + ATYP(1) + IPv4(4) + Port(2) = 10
+ {
+ throw new ArgumentException("Invalid SOCKS5 UDP packet: too short");
+ }
+
+ var offset = 0;
+
+ // RSV (2 bytes) - Reserved field, skip
+ offset += 2;
+
+ // FRAG (1 byte) - Fragment number, currently only support 0 (no fragmentation)
+ var frag = packet[offset++];
+ if (frag != 0x00)
+ {
+ throw new NotSupportedException("SOCKS5 UDP fragmentation is not supported");
+ }
+
+ // ATYP (1 byte) - Address type
+ var addressType = packet[offset++];
+
+ string host;
+ int addressLength;
+ bool isDomain;
+
+ switch (addressType)
+ {
+ case Socks5AddressData.AddrTypeIPv4:
+ if (packet.Length < offset + 4)
+ {
+ throw new ArgumentException("Invalid SOCKS5 UDP packet: IPv4 address incomplete");
+ }
+
+ var ipv4Bytes = new byte[4];
+ Array.Copy(packet, offset, ipv4Bytes, 0, 4);
+ host = new IPAddress(ipv4Bytes).ToString();
+ addressLength = 4;
+ isDomain = false;
+ break;
+
+ case Socks5AddressData.AddrTypeIPv6:
+ if (packet.Length < offset + 16)
+ {
+ throw new ArgumentException("Invalid SOCKS5 UDP packet: IPv6 address incomplete");
+ }
+
+ var ipv6Bytes = new byte[16];
+ Array.Copy(packet, offset, ipv6Bytes, 0, 16);
+ host = new IPAddress(ipv6Bytes).ToString();
+ addressLength = 16;
+ isDomain = false;
+ break;
+
+ case Socks5AddressData.AddrTypeDomain:
+ if (packet.Length < offset + 1)
+ {
+ throw new ArgumentException("Invalid SOCKS5 UDP packet: domain length missing");
+ }
+
+ var domainLength = packet[offset++];
+ if (packet.Length < offset + domainLength)
+ {
+ throw new ArgumentException("Invalid SOCKS5 UDP packet: domain incomplete");
+ }
+
+ host = Encoding.ASCII.GetString(packet, offset, domainLength);
+ addressLength = domainLength;
+ isDomain = true;
+ break;
+
+ default:
+ throw new NotSupportedException($"Unsupported SOCKS5 address type: {addressType}");
+ }
+
+ offset += addressLength;
+
+ // Port (2 bytes, big-endian)
+ if (packet.Length < offset + 2)
+ {
+ throw new ArgumentException("Invalid SOCKS5 UDP packet: port incomplete");
+ }
+
+ var port = BinaryPrimitives.ReadUInt16BigEndian(packet.AsSpan(offset, 2));
+ offset += 2;
+
+ // Data (remaining bytes)
+ var dataLength = packet.Length - offset;
+ var data = new byte[dataLength];
+ if (dataLength > 0)
+ {
+ Array.Copy(packet, offset, data, 0, dataLength);
+ }
+
+ // Create remote endpoint without DNS resolution
+ var remote = new Socks5RemoteEndpoint(host, port, isDomain);
+ return (remote, data);
+ }
+
+ public void Dispose()
+ {
+ _tcpClient.Dispose();
+ _udpClient.Dispose();
+ }
+
+ #region SOCKS5 Connection Handling
+
+ private const byte Socks5Version = 0x05;
+ private const byte SocksCmdUdpAssociate = 0x03;
+
+ public async Task EstablishUdpAssociationAsync(CancellationToken cancellationToken)
+ {
+ if (_initialized)
+ {
+ Dispose();
+ _initialized = false;
+ }
+
+ _udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
+ _tcpClient = new TcpClient();
+ try
+ {
+ await _tcpClient.ConnectAsync(socks5Host, socks5TcpPort, cancellationToken).ConfigureAwait(false);
+ }
+ catch (SocketException)
+ {
+ return false;
+ }
+
+ var tcpControlStream = _tcpClient.GetStream();
+
+ byte[] handshakeRequest = [Socks5Version, 0x01, 0x00];
+ await tcpControlStream.WriteAsync(handshakeRequest, cancellationToken).ConfigureAwait(false);
+ var handshakeResponse = new byte[2];
+ if (await tcpControlStream.ReadAsync(handshakeResponse, cancellationToken).ConfigureAwait(false) < 2 ||
+ handshakeResponse[0] != Socks5Version || handshakeResponse[1] != 0x00)
+ {
+ return false;
+ }
+
+ var clientAddrForSocks = new Socks5AddressData
+ {
+ AddressType = Socks5AddressData.AddrTypeIPv4,
+ Host = "0.0.0.0",
+ Port = 0
+ };
+ using var udpAssociateReqMs = new MemoryStream();
+ udpAssociateReqMs.WriteByte(Socks5Version);
+ udpAssociateReqMs.WriteByte(SocksCmdUdpAssociate);
+ udpAssociateReqMs.WriteByte(0x00);
+ udpAssociateReqMs.Write(clientAddrForSocks.ToBytes());
+ await tcpControlStream.WriteAsync(udpAssociateReqMs.ToArray(), cancellationToken).ConfigureAwait(false);
+
+ var verRepRsv = new byte[3];
+ if (await tcpControlStream.ReadAsync(verRepRsv, cancellationToken).ConfigureAwait(false) < 3 ||
+ verRepRsv[0] != Socks5Version || verRepRsv[1] != 0x00)
+ {
+ return false;
+ }
+
+ var proxyRelaySocksAddr =
+ await Socks5AddressData.ParseAsync(tcpControlStream, cancellationToken).ConfigureAwait(false);
+ if (proxyRelaySocksAddr == null || !IPAddress.TryParse(proxyRelaySocksAddr.Host, out var proxyRelayIp))
+ {
+ return false;
+ }
+
+ _relayEndPoint = new IPEndPoint(proxyRelayIp, proxyRelaySocksAddr.Port);
+ _initialized = true;
+ return true;
+ }
+
+ #endregion SOCKS5 Connection Handling
+
+ #region SOCKS5 Address Handling
+
+ private class Socks5AddressData
+ {
+ public const byte AddrTypeIPv4 = 0x01;
+ public const byte AddrTypeDomain = 0x03;
+ public const byte AddrTypeIPv6 = 0x04;
+
+ public byte AddressType { get; set; }
+ public string Host { get; set; } = string.Empty;
+ public ushort Port { get; set; }
+
+ public byte[] ToBytes()
+ {
+ using var ms = new MemoryStream();
+ ms.WriteByte(AddressType);
+ switch (AddressType)
+ {
+ case AddrTypeIPv4:
+ if (IPAddress.TryParse(Host, out var ip) && ip.AddressFamily == AddressFamily.InterNetwork)
+ {
+ ms.Write(ip.GetAddressBytes(), 0, 4);
+ }
+ else
+ {
+ ms.Write([0, 0, 0, 0]);
+ }
+
+ break;
+
+ case AddrTypeDomain:
+ if (string.IsNullOrEmpty(Host))
+ {
+ ms.WriteByte(0);
+ }
+ else
+ {
+ var domainBytes = Encoding.ASCII.GetBytes(Host);
+ ms.WriteByte((byte)domainBytes.Length);
+ ms.Write(domainBytes);
+ }
+
+ break;
+
+ case AddrTypeIPv6:
+ if (IPAddress.TryParse(Host, out var ip6) && ip6.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ ms.Write(ip6.GetAddressBytes(), 0, 16);
+ }
+ else
+ {
+ ms.Write(new byte[16]);
+ }
+
+ break;
+
+ default:
+ throw new NotSupportedException($"SOCKS5 address type {AddressType} not supported.");
+ }
+
+ var portBytes = new byte[2];
+ BinaryPrimitives.WriteUInt16BigEndian(portBytes, Port);
+ ms.Write(portBytes);
+ return ms.ToArray();
+ }
+
+ public static async Task ParseAsync(Stream stream, CancellationToken ct)
+ {
+ var addr = new Socks5AddressData();
+ var typeByte = new byte[1];
+ try
+ {
+ if (await stream.ReadAsync(typeByte.AsMemory(0, 1), ct).ConfigureAwait(false) < 1)
+ {
+ return null;
+ }
+
+ addr.AddressType = typeByte[0];
+ switch (addr.AddressType)
+ {
+ case AddrTypeIPv4:
+ var ipv4Bytes = new byte[4];
+ if (await stream.ReadAsync(ipv4Bytes.AsMemory(0, 4), ct).ConfigureAwait(false) < 4)
+ {
+ return null;
+ }
+
+ 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)
+ {
+ return null;
+ }
+
+ if (lenByte[0] == 0)
+ {
+ addr.Host = string.Empty;
+ }
+ else
+ {
+ var domainBytes = new byte[lenByte[0]];
+ if (await stream.ReadAsync(domainBytes.AsMemory(0, domainBytes.Length), ct)
+ .ConfigureAwait(false) < domainBytes.Length)
+ {
+ return null;
+ }
+
+ addr.Host = Encoding.ASCII.GetString(domainBytes);
+ }
+
+ break;
+
+ case AddrTypeIPv6:
+ var ipv6Bytes = new byte[16];
+ if (await stream.ReadAsync(ipv6Bytes.AsMemory(0, 16), ct).ConfigureAwait(false) < 16)
+ {
+ return null;
+ }
+
+ addr.Host = new IPAddress(ipv6Bytes).ToString();
+ break;
+
+ default:
+ return null;
+ }
+
+ var portBytes = new byte[2];
+ if (await stream.ReadAsync(portBytes.AsMemory(0, 2), ct).ConfigureAwait(false) < 2)
+ {
+ return null;
+ }
+
+ addr.Port = BinaryPrimitives.ReadUInt16BigEndian(portBytes);
+ return addr;
+ }
+ catch (Exception ex) when (ex is IOException or ObjectDisposedException)
+ {
+ return null;
+ }
+ }
+ }
+
+ #endregion SOCKS5 Address Handling
+}
diff --git a/v2rayN/ServiceLib.UdpTest/Tester/DnsService.cs b/v2rayN/ServiceLib.UdpTest/Tester/DnsService.cs
new file mode 100644
index 00000000..81bea2b8
--- /dev/null
+++ b/v2rayN/ServiceLib.UdpTest/Tester/DnsService.cs
@@ -0,0 +1,77 @@
+namespace ServiceLib.UdpTest.Tester;
+
+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
+ 0x12, 0x34, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ // Question: www.google.com, Type A, Class IN
+ 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F,
+ 0x67, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00,
+ 0x00, 0x01, 0x00, 0x01
+ ];
+
+ public byte[] BuildUdpRequestPacket()
+ {
+ return (byte[])DnsQueryPacket.Clone();
+ }
+
+ public bool VerifyAndExtractUdpResponse(byte[] dnsResponseBytes)
+ {
+ if (dnsResponseBytes.Length < 12)
+ {
+ return false;
+ }
+
+ try
+ {
+ // Check transaction ID (should match 0x1234)
+ var transactionId = BinaryPrimitives.ReadUInt16BigEndian(dnsResponseBytes.AsSpan(0, 2));
+ if (transactionId != 0x1234)
+ {
+ return false;
+ }
+
+ // Check flags - should be a response (QR=1)
+ var flags = BinaryPrimitives.ReadUInt16BigEndian(dnsResponseBytes.AsSpan(2, 2));
+ if ((flags & 0x8000) == 0)
+ {
+ return false; // Not a response
+ }
+
+ // Check response code (RCODE) - should be 0 (no error)
+ if ((flags & 0x000F) != 0)
+ {
+ return false; // DNS error
+ }
+
+ // Check answer count
+ var answerCount = BinaryPrimitives.ReadUInt16BigEndian(dnsResponseBytes.AsSpan(6, 2));
+ if (answerCount == 0)
+ {
+ return false; // No answers
+ }
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public ushort GetDefaultTargetPort()
+ {
+ return DnsDefaultPort;
+ }
+
+ public string GetDefaultTargetHost()
+ {
+ return DnsDefaultServer;
+ }
+}
diff --git a/v2rayN/ServiceLib.UdpTest/Tester/IUdpTest.cs b/v2rayN/ServiceLib.UdpTest/Tester/IUdpTest.cs
new file mode 100644
index 00000000..af3fa6c5
--- /dev/null
+++ b/v2rayN/ServiceLib.UdpTest/Tester/IUdpTest.cs
@@ -0,0 +1,12 @@
+namespace ServiceLib.UdpTest.Tester;
+
+public interface IUdpTest
+{
+ public byte[] BuildUdpRequestPacket();
+
+ public bool VerifyAndExtractUdpResponse(byte[] udpResponseBytes);
+
+ public ushort GetDefaultTargetPort();
+
+ public string GetDefaultTargetHost();
+}
diff --git a/v2rayN/ServiceLib.UdpTest/Tester/McBeService.cs b/v2rayN/ServiceLib.UdpTest/Tester/McBeService.cs
new file mode 100644
index 00000000..b4ec0221
--- /dev/null
+++ b/v2rayN/ServiceLib.UdpTest/Tester/McBeService.cs
@@ -0,0 +1,84 @@
+namespace ServiceLib.UdpTest.Tester;
+
+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 =
+ [
+ // 0x01
+ 0x01,
+ // Client alive time (1000 ms)
+ 0x27, 0xC4, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // Magic
+ 0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE,
+ 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
+ // Client GUID (random 16 bytes)
+ 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 ValidGameModes =
+ [
+ "Survival",
+ "Creative",
+ "Adventure",
+ "Spectator"
+ ];
+
+ public byte[] BuildUdpRequestPacket()
+ {
+ return (byte[])McBeQueryPacket.Clone();
+ }
+
+ public bool VerifyAndExtractUdpResponse(byte[] mcbeResponseBytes)
+ {
+ // 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)
+ {
+ return false;
+ }
+ if (mcbeResponseBytes[0] != 0x1C)
+ {
+ return false; // Invalid packet type
+ }
+ var pongMagic = mcbeResponseBytes.Skip(17).Take(16).ToArray();
+ if (!pongMagic.SequenceEqual(McBeMagicBytes))
+ {
+ return false; // Magic bytes do not match
+ }
+ var stringLength = (ushort)((mcbeResponseBytes[33] << 8) | mcbeResponseBytes[34]);
+ var stringData = Encoding.UTF8.GetString(mcbeResponseBytes.Skip(35).Take(stringLength).ToArray());
+ var stringParts = stringData.Split(';');
+ // check Game Mode str
+ var gameMode = stringParts.Length > 8 ? stringParts[8] : "";
+ if (!ValidGameModes.Contains(gameMode))
+ {
+ return false; // Invalid game mode
+ }
+ return true;
+ }
+
+ public ushort GetDefaultTargetPort()
+ {
+ return McBeDefaultPort;
+ }
+
+ public string GetDefaultTargetHost()
+ {
+ return McBeDefaultServer;
+ }
+}
diff --git a/v2rayN/ServiceLib.UdpTest/Tester/NtpService.cs b/v2rayN/ServiceLib.UdpTest/Tester/NtpService.cs
new file mode 100644
index 00000000..2421e484
--- /dev/null
+++ b/v2rayN/ServiceLib.UdpTest/Tester/NtpService.cs
@@ -0,0 +1,37 @@
+namespace ServiceLib.UdpTest.Tester;
+
+public class NtpService : IUdpTest
+{
+ private const int NtpDefaultPort = 123;
+ private const string NtpDefaultServer = "pool.ntp.org";
+
+ public byte[] BuildUdpRequestPacket()
+ {
+ var ntpReq = new byte[48];
+ ntpReq[0] = 0x23; // LI=0, VN=4, Mode=3
+ return ntpReq;
+ }
+
+ public bool VerifyAndExtractUdpResponse(byte[] ntpResponseBytes)
+ {
+ if (ntpResponseBytes.Length < 48)
+ {
+ return false;
+ }
+ if ((ntpResponseBytes[0] & 0x07) != 4)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public ushort GetDefaultTargetPort()
+ {
+ return NtpDefaultPort;
+ }
+
+ public string GetDefaultTargetHost()
+ {
+ return NtpDefaultServer;
+ }
+}
diff --git a/v2rayN/ServiceLib.UdpTest/Tester/StunService.cs b/v2rayN/ServiceLib.UdpTest/Tester/StunService.cs
new file mode 100644
index 00000000..c6b925a3
--- /dev/null
+++ b/v2rayN/ServiceLib.UdpTest/Tester/StunService.cs
@@ -0,0 +1,52 @@
+namespace ServiceLib.UdpTest.Tester;
+
+public class StunService : IUdpTest
+{
+ private const int StunDefaultPort = 3478;
+ private const string StunDefaultServer = "stun.voztovoice.org";
+
+ private static readonly byte[] StunBindingRequestPacket =
+ [
+ // STUN Binding Request
+ 0x00, 0x01, // Message Type: Binding Request (0x0001)
+ 0x00, 0x00, // Message Length: 0 (no attributes)
+ 0x21, 0x12, 0xA4, 0x42, // Magic Cookie: 0x2112A442
+ // Transaction ID: 96 bits (12 bytes) random
+ 0x66, 0x0E, 0xAB, 0xBC, 0x61, 0x0D,
+ 0xA4, 0x40, 0x8C, 0x65, 0xC1, 0xBE,
+ ];
+
+ public byte[] BuildUdpRequestPacket()
+ {
+ return (byte[])StunBindingRequestPacket.Clone();
+ }
+
+ public bool VerifyAndExtractUdpResponse(byte[] stunResponseBytes)
+ {
+ if (stunResponseBytes.Length < 20)
+ {
+ return false;
+ }
+
+ if (stunResponseBytes.Length >= 2)
+ {
+ var messageType = (stunResponseBytes[0] << 8) | stunResponseBytes[1];
+ if (messageType is 0x0101 or 0x0111)
+ {
+ return true;
+ }
+ }
+
+ return true;
+ }
+
+ public ushort GetDefaultTargetPort()
+ {
+ return StunDefaultPort;
+ }
+
+ public string GetDefaultTargetHost()
+ {
+ return StunDefaultServer;
+ }
+}
diff --git a/v2rayN/ServiceLib.UdpTest/UdpTestService.cs b/v2rayN/ServiceLib.UdpTest/UdpTestService.cs
new file mode 100644
index 00000000..be4b943d
--- /dev/null
+++ b/v2rayN/ServiceLib.UdpTest/UdpTestService.cs
@@ -0,0 +1,154 @@
+using ServiceLib.UdpTest.Tester;
+
+namespace ServiceLib.UdpTest;
+
+public class UdpTestService
+{
+ private const string DefaultUdpTestType = "ntp";
+ private readonly IUdpTest _udpTest;
+
+ private static readonly IReadOnlyDictionary> UdpTestFactories =
+ new Dictionary>(StringComparer.OrdinalIgnoreCase)
+ {
+ ["ntp"] = () => new NtpService(),
+ ["dns"] = () => new DnsService(),
+ ["stun"] = () => new StunService(),
+ ["mcbe"] = () => new McBeService(),
+ };
+
+ private UdpTestService(IUdpTest udpTest)
+ {
+ _udpTest = udpTest;
+ }
+
+ public static UdpTestService Create(string? udpTestType)
+ {
+ if (string.IsNullOrEmpty(udpTestType))
+ {
+ return new UdpTestService(UdpTestFactories[DefaultUdpTestType]());
+ }
+
+ return UdpTestFactories.TryGetValue(udpTestType, out var factory)
+ ? new UdpTestService(factory())
+ : new UdpTestService(UdpTestFactories[DefaultUdpTestType]());
+ }
+
+ public static UdpTestService CreateFromTarget(string? udpTestTarget, out string targetServerHost)
+ {
+ var parts = udpTestTarget?.Split(':', 2);
+ var udpTestType = parts?.Length > 0 ? parts[0] : DefaultUdpTestType;
+
+ var udpService = Create(udpTestType);
+ targetServerHost = parts?.Length > 1 && !string.IsNullOrEmpty(parts[1])
+ ? parts[1]
+ : udpService._udpTest.GetDefaultTargetHost();
+
+ return udpService;
+ }
+
+ private (string host, ushort port) ParseHostAndPort(string targetServerHost)
+ {
+ if (string.IsNullOrEmpty(targetServerHost))
+ {
+ return (_udpTest.GetDefaultTargetHost(), _udpTest.GetDefaultTargetPort());
+ }
+
+ // Handle IPv6 format: [::1]:port or [2001:db8::1]:port
+ if (targetServerHost.StartsWith('['))
+ {
+ var closeBracketIndex = targetServerHost.IndexOf(']');
+ if (closeBracketIndex > 0)
+ {
+ var host = targetServerHost.Substring(1, closeBracketIndex - 1);
+ if (closeBracketIndex < targetServerHost.Length - 1 && targetServerHost[closeBracketIndex + 1] == ':')
+ {
+ var portStr = targetServerHost.Substring(closeBracketIndex + 2);
+ if (ushort.TryParse(portStr, out var port))
+ {
+ return (host, port);
+ }
+ }
+ return (host, _udpTest.GetDefaultTargetPort());
+ }
+ }
+
+ // Handle IPv4 or domain format: 1.1.1.1:53 or exam.com:333
+ var lastColonIndex = targetServerHost.LastIndexOf(':');
+ if (lastColonIndex > 0)
+ {
+ var host = targetServerHost.Substring(0, lastColonIndex);
+ var portStr = targetServerHost.Substring(lastColonIndex + 1);
+ if (ushort.TryParse(portStr, out var port))
+ {
+ return (host, port);
+ }
+ }
+
+ // No port specified, use default
+ return (targetServerHost, _udpTest.GetDefaultTargetPort());
+ }
+
+ public async Task SendUdpRequestAsync(string targetServerHost, int socks5Port, TimeSpan operationTimeout)
+ {
+ using var cts = new CancellationTokenSource(operationTimeout);
+ var cancellationToken = cts.Token;
+ var udpRequestPacket = _udpTest.BuildUdpRequestPacket();
+ if (udpRequestPacket == null || udpRequestPacket.Length == 0)
+ {
+ throw new InvalidOperationException("Failed to build UDP request packet.");
+ }
+ using var channel = new Socks5UdpChannel("127.0.0.1", socks5Port);
+ if (!await channel.EstablishUdpAssociationAsync(cancellationToken).ConfigureAwait(false))
+ {
+ throw new Exception("Failed to establish UDP association with SOCKS5 proxy.");
+ }
+
+ var (targetHost, targetPort) = ParseHostAndPort(targetServerHost);
+
+ byte[] udpReceiveResult = null;
+
+ // Get minimum round trip time from two attempts
+ var roundTripTime = TimeSpan.MaxValue;
+
+ for (var attempt = 0; attempt < 2; attempt++)
+ {
+ try
+ {
+ var stopwatch = new Stopwatch();
+ stopwatch.Start();
+ await channel.SendAsync(targetHost, targetPort, udpRequestPacket).ConfigureAwait(false);
+ var (_, receiveResult) = await channel.ReceiveAsync(cancellationToken).ConfigureAwait(false);
+ stopwatch.Stop();
+
+ udpReceiveResult = receiveResult;
+
+ var currentRoundTripTime = stopwatch.Elapsed;
+ if (currentRoundTripTime < roundTripTime)
+ {
+ roundTripTime = currentRoundTripTime;
+ }
+ }
+ catch
+ {
+ if (attempt == 1 && roundTripTime == TimeSpan.MaxValue)
+ {
+ throw;
+ }
+ }
+ }
+
+ if ((udpReceiveResult?.Length ?? 0) < 4 + 1 + 4 + 2)
+ {
+ throw new Exception("Received NTP response is too short.");
+ }
+
+ if (udpReceiveResult != null && _udpTest.VerifyAndExtractUdpResponse(udpReceiveResult))
+ {
+ return roundTripTime;
+ }
+ else
+ {
+ throw new Exception("Failed to verify and extract UDP response.");
+ }
+ }
+}
diff --git a/v2rayN/ServiceLib/Enums/ESpeedActionType.cs b/v2rayN/ServiceLib/Enums/ESpeedActionType.cs
index a03aa9df..5a631734 100644
--- a/v2rayN/ServiceLib/Enums/ESpeedActionType.cs
+++ b/v2rayN/ServiceLib/Enums/ESpeedActionType.cs
@@ -4,6 +4,7 @@ public enum ESpeedActionType
{
Tcping,
Realping,
+ UdpTest,
Speedtest,
Mixedtest,
FastRealping
diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs
index a20a0ba5..4d4f541a 100644
--- a/v2rayN/ServiceLib/Global.cs
+++ b/v2rayN/ServiceLib/Global.cs
@@ -67,6 +67,8 @@ public class Global
public const string AsIs = "AsIs";
public const string IPIfNonMatch = "IPIfNonMatch";
public const string IPOnDemand = "IPOnDemand";
+ public const string GeoSitePrefix = "geosite:";
+ public const string GeoIPPrefix = "geoip:";
public const string UserEMail = "t@t.tt";
public const string AutoRunRegPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
@@ -640,6 +642,24 @@ public class Global
@""
];
+ public static readonly List UdpTestTargets =
+ [
+ "ntp:pool.ntp.org",
+ "ntp:time.google.com",
+ "dns:1.1.1.1",
+ "dns:8.8.8.8",
+ "dns:dns.google",
+ "stun:stun.voztovoice.org",
+ "stun:stun.cloudflare.com",
+ "stun:stun.l.google.com:19302",
+ "mcbe:pms.mc-complex.com",
+ "mcbe:bedrock.opblocks.com",
+ "mcbe:opsucht.net",
+ "mcbe:play.craftersmc.net",
+ "mcbe:mps.lemoncloud.net",
+ "mcbe:bedrock.talonmc.net",
+ ];
+
public static readonly List OutboundTags =
[
ProxyTag,
@@ -673,14 +693,6 @@ public class Global
""
];
- public static readonly List EchForceQuerys =
- [
- "none",
- "half",
- "full",
- ""
- ];
-
public static readonly List TunIcmpRoutingPolicies =
[
"rule",
diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
index 281db07d..5c6d7028 100644
--- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs
+++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
@@ -135,6 +135,10 @@ public static class ConfigHandler
{
config.SpeedTestItem.MixedConcurrencyCount = 5;
}
+ if (config.SpeedTestItem.UdpTestTarget.IsNullOrEmpty())
+ {
+ config.SpeedTestItem.UdpTestTarget = Global.UdpTestTargets.First();
+ }
config.Mux4RayItem ??= new()
{
@@ -252,7 +256,6 @@ public static class ConfigHandler
item.Cert = profileItem.Cert;
item.CertSha = profileItem.CertSha;
item.EchConfigList = profileItem.EchConfigList;
- item.EchForceQuery = profileItem.EchForceQuery;
item.Finalmask = profileItem.Finalmask;
item.ProtoExtra = profileItem.ProtoExtra;
item.TransportExtra = profileItem.TransportExtra;
@@ -702,10 +705,12 @@ public static class ConfigHandler
public static async Task AddHysteria2Server(Config config, ProfileItem profileItem, bool toFile = true)
{
profileItem.ConfigType = EConfigType.Hysteria2;
- //profileItem.CoreType = ECoreType.sing_box;
profileItem.Address = profileItem.Address.TrimEx();
profileItem.Password = profileItem.Password.TrimEx();
+ profileItem.Fingerprint = string.Empty;
+ profileItem.Alpn = string.Empty;
+ //profileItem.Alpn = "h3";
profileItem.Network = string.Empty;
if (profileItem.StreamSecurity.IsNullOrEmpty())
@@ -745,6 +750,7 @@ public static class ConfigHandler
profileItem.Username = profileItem.Username.TrimEx();
profileItem.Password = profileItem.Password.TrimEx();
profileItem.Network = string.Empty;
+ profileItem.Fingerprint = string.Empty;
var congestionControl = profileItem.GetProtocolExtra().CongestionControl;
if (!Global.TuicCongestionControls.Contains(congestionControl))
@@ -848,8 +854,10 @@ public static class ConfigHandler
profileItem.Address = profileItem.Address.TrimEx();
profileItem.Username = profileItem.Username.TrimEx();
profileItem.Password = profileItem.Password.TrimEx();
+ profileItem.Fingerprint = string.Empty;
profileItem.Alpn = string.Empty;
profileItem.Network = string.Empty;
+ profileItem.AllowInsecure = "false";
if (profileItem.StreamSecurity.IsNullOrEmpty())
{
profileItem.StreamSecurity = Global.StreamSecurity;
@@ -1037,13 +1045,19 @@ public static class ConfigHandler
foreach (var item in lstProfile)
{
- if (!lstKeep.Exists(i => CompareProfileItem(i, item, false)))
+ if (item.IsComplex())
{
lstKeep.Add(item);
+ continue;
+ }
+
+ if (lstKeep.Exists(i => CompareProfileItem(i, item, false)))
+ {
+ lstRemove.Add(item);
}
else
{
- lstRemove.Add(item);
+ lstKeep.Add(item);
}
}
await RemoveServers(config, lstRemove);
diff --git a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs
index 3f07548f..ff7d6d20 100644
--- a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs
+++ b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs
@@ -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)
@@ -119,6 +120,10 @@ public class BaseFmt
{
dicQuery.Add("seed", UrlEncodeSafe(transport.KcpSeed));
}
+ if (transport.KcpMtu > 0)
+ {
+ dicQuery.Add("mtu", transport.KcpMtu.ToString());
+ }
break;
case nameof(ETransport.ws):
@@ -279,10 +284,13 @@ public class BaseFmt
case nameof(ETransport.kcp):
var kcpSeed = GetQueryDecoded(query, "seed");
+ var kcpMtuStr = GetQueryValue(query, "mtu");
+ var kcpMtu = int.TryParse(kcpMtuStr, out var mtu) ? mtu : 0;
transport = transport with
{
KcpHeaderType = GetQueryValue(query, "headerType", Global.None),
KcpSeed = kcpSeed,
+ KcpMtu = kcpMtu > 0 ? mtu : null,
};
break;
diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs
index 1988d3b7..e54f1a7a 100644
--- a/v2rayN/ServiceLib/Models/ConfigItems.cs
+++ b/v2rayN/ServiceLib/Models/ConfigItems.cs
@@ -156,6 +156,7 @@ public class SpeedTestItem
public string SpeedPingTestUrl { get; set; }
public int MixedConcurrencyCount { get; set; }
public string IPAPIUrl { get; set; }
+ public string UdpTestTarget { get; set; }
}
[Serializable]
diff --git a/v2rayN/ServiceLib/Models/ProfileItem.cs b/v2rayN/ServiceLib/Models/ProfileItem.cs
index 6fe44a3b..ae5f77ad 100644
--- a/v2rayN/ServiceLib/Models/ProfileItem.cs
+++ b/v2rayN/ServiceLib/Models/ProfileItem.cs
@@ -191,7 +191,6 @@ public class ProfileItem
public string Cert { get; set; }
public string CertSha { get; set; }
public string EchConfigList { get; set; }
- public string EchForceQuery { get; set; }
public string Finalmask { get; set; }
public string ProtoExtra { get; set; }
diff --git a/v2rayN/ServiceLib/Models/SingboxConfig.cs b/v2rayN/ServiceLib/Models/SingboxConfig.cs
index 06449224..2282e830 100644
--- a/v2rayN/ServiceLib/Models/SingboxConfig.cs
+++ b/v2rayN/ServiceLib/Models/SingboxConfig.cs
@@ -237,6 +237,7 @@ public class Transport4Sbox
public class Headers4Sbox
{
public string? Host { get; set; }
+
[JsonPropertyName("User-Agent")]
public string UserAgent { get; set; }
}
diff --git a/v2rayN/ServiceLib/Models/TransportExtraItem.cs b/v2rayN/ServiceLib/Models/TransportExtraItem.cs
index 7ffcc9f5..77fbc984 100644
--- a/v2rayN/ServiceLib/Models/TransportExtraItem.cs
+++ b/v2rayN/ServiceLib/Models/TransportExtraItem.cs
@@ -15,4 +15,5 @@ public record TransportExtraItem
public string? KcpHeaderType { get; init; }
public string? KcpSeed { get; init; }
+ public int? KcpMtu { get; init; }
}
diff --git a/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayN/ServiceLib/Models/V2rayConfig.cs
index b6aaa2df..26739b69 100644
--- a/v2rayN/ServiceLib/Models/V2rayConfig.cs
+++ b/v2rayN/ServiceLib/Models/V2rayConfig.cs
@@ -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? 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; }
}
diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs
index 74691c67..688d7e6c 100644
--- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs
+++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs
@@ -106,7 +106,7 @@ namespace ServiceLib.Resx {
}
///
- /// 查找类似 Please check the Configuration settings first. 的本地化字符串。
+ /// 查找类似 Invalid configuration, please check or reselect 的本地化字符串。
///
public static string CheckServerSettings {
get {
@@ -1860,6 +1860,15 @@ namespace ServiceLib.Resx {
}
}
+ ///
+ /// 查找类似 Test Configurations UDP Delay 的本地化字符串。
+ ///
+ public static string menuUdpTestServer {
+ get {
+ return ResourceManager.GetString("menuUdpTestServer", resourceCulture);
+ }
+ }
+
///
/// 查找类似 {0} Website 的本地化字符串。
///
@@ -2872,7 +2881,7 @@ namespace ServiceLib.Resx {
}
///
- /// 查找类似 Supports DNS Object; Click to view documentation 的本地化字符串。
+ /// 查找类似 Please fill in DNS Object; Click to view documentation 的本地化字符串。
///
public static string TbDnsObjectDoc {
get {
@@ -2934,15 +2943,6 @@ namespace ServiceLib.Resx {
}
}
- ///
- /// 查找类似 EchForceQuery 的本地化字符串。
- ///
- public static string TbEchForceQuery {
- get {
- return ResourceManager.GetString("TbEchForceQuery", resourceCulture);
- }
- }
-
///
/// 查找类似 Edit 的本地化字符串。
///
@@ -4149,15 +4149,6 @@ namespace ServiceLib.Resx {
}
}
- ///
- /// 查找类似 Custom DNS (multiple, separated by commas (,)) 的本地化字符串。
- ///
- public static string TbSettingsRemoteDNS {
- get {
- return ResourceManager.GetString("TbSettingsRemoteDNS", resourceCulture);
- }
- }
-
///
/// 查找类似 Route Only 的本地化字符串。
///
@@ -4392,6 +4383,15 @@ namespace ServiceLib.Resx {
}
}
+ ///
+ /// 查找类似 UDP Test Url 的本地化字符串。
+ ///
+ public static string TbSettingsUdpTestUrl {
+ get {
+ return ResourceManager.GetString("TbSettingsUdpTestUrl", resourceCulture);
+ }
+ }
+
///
/// 查找类似 Auth user 的本地化字符串。
///
diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx
index f0878156..f2818543 100644
--- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx
+++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx
@@ -720,9 +720,6 @@
مجوز احراز هویت
-
- سفارشی DNS (multiple, separated by commas (,))
-
تنظیم کردن Win10 UWP Loopback
@@ -1584,9 +1581,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
EchConfigList
-
- EchForceQuery
-
Full certificate (chain), PEM format
@@ -1713,4 +1707,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
Only for fetching self-signed certificates. This may expose you to MITM risks.
+
+ Test Configurations UDP Delay
+
+
+ UDP Test Url
+
\ No newline at end of file
diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx
index 9533c895..1a782f98 100644
--- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx
+++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx
@@ -720,9 +720,6 @@
Mot de passe d’authentification
-
- DNS perso (plusieurs configurables, séparés par virgules)
-
Lever la restriction de proxy en boucle locale pour les applications Win10 UWP
@@ -1590,9 +1587,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
EchConfigList
-
- EchForceQuery
-
Certificat complet (chaîne), format PEM
diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx
index cbc77d26..dbd24707 100644
--- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx
+++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx
@@ -720,9 +720,6 @@
Hitelesítési jelszó
-
- Egyéni DNS (több, vesszővel (,) elválasztva)
-
Win10 UWP Loopback beállítása
@@ -1584,9 +1581,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
EchConfigList
-
- EchForceQuery
-
Full certificate (chain), PEM format
@@ -1713,4 +1707,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
Only for fetching self-signed certificates. This may expose you to MITM risks.
+
+ Test Configurations UDP Delay
+
+
+ UDP Test Url
+
\ No newline at end of file
diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx
index 5234ed03..6ceef3c1 100644
--- a/v2rayN/ServiceLib/Resx/ResUI.resx
+++ b/v2rayN/ServiceLib/Resx/ResUI.resx
@@ -121,7 +121,7 @@
Export share link to clipboard successfully
- Please check the Configuration settings first.
+ Invalid configuration, please check or reselect
Invalid configuration format.
@@ -720,9 +720,6 @@
Auth pass
-
- Custom DNS (multiple, separated by commas (,))
-
Set Win10 UWP Loopback
@@ -862,7 +859,7 @@
Rule object Doc
- Supports DNS Object; Click to view documentation
+ Please fill in DNS Object; Click to view documentation
For group please leave blank here
@@ -1593,9 +1590,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
EchConfigList
-
- EchForceQuery
-
Full certificate (chain), PEM format
@@ -1719,4 +1713,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
Only for fetching self-signed certificates. This may expose you to MITM risks.
-
\ No newline at end of file
+
+ Test Configurations UDP Delay
+
+
+ UDP Test Url
+
+
diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx
index 13adbf49..8f234843 100644
--- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx
+++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx
@@ -673,7 +673,7 @@
Ядро: базовые настройки
- Пользовательский DNS для V2ray
+ Пользовательский DNS для v2ray
Ядро: настройки KCP
@@ -720,9 +720,6 @@
Пароль авторизации
-
- Пользовательский DNS (если несколько, то делите запятыми (,))
-
Разрешить loopback для приложений UWP (Win10)
@@ -934,7 +931,7 @@
User-Agent
- This parameter is valid only for raw/http, ws, gRPC and xhttp
+ Параметр действует только для raw/http, ws, gRPC и xhttp
Шрифт (требуется перезапуск)
@@ -1321,7 +1318,7 @@
XHTTP-режим
- Raw JSON, format: { XHTTP Object }
+ Сырой JSON, формат: { XHTTP Object }
Сворачивать в трей при закрытии окна
@@ -1339,7 +1336,7 @@
Включить второй смешанный порт
- socks: локальный порт, socks2: второй локальный порт, socks3: LAN порт
+ socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт
Тема
@@ -1381,7 +1378,7 @@
Mldsa65Verify
- Добавить сервер [Anytls]
+ Добавить сервер [AnyTLS]
Удалённый DNS
@@ -1584,9 +1581,6 @@
EchConfigList
-
- EchForceQuery
-
Полный сертификат (цепочка) в формате PEM
@@ -1696,21 +1690,33 @@
Устаревшая защита TUN (Legacy Protect)
- For multi-interface environments, enter the local machine's IPv4 address
+ Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера
Камуфляжный домен
- Host
+ Хост
- XHTTP Extra
+ Дополнительные параметры XHTTP (Extra)
- Allow insecure cert fetch (self-signed)
+ Разрешить небезопасную загрузку сертификата (самоподписанного)
- Only for fetching self-signed certificates. This may expose you to MITM risks.
+ Только для загрузки самоподписанных сертификатов. Это может подвергнуть вас риску атаки «человек посередине» (MITM).
-
\ No newline at end of file
+
+ Тест UDP-задержки конфигураций
+
+
+ URL для UDP-теста
+
+
+ Укажите корректный IPv4-адрес для SendThrough.
+
+
+ Локальный исходящий адрес (SendThrough)
+
+
diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx
index 42a4552b..759596e9 100644
--- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx
+++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx
@@ -121,7 +121,7 @@
导出分享链接至剪贴板成功
- 请先检查设置
+ 配置项无效,请检查或重新选择
配置格式不正确
@@ -720,9 +720,6 @@
认证密码
-
- 自定义 DNS (可多个,用逗号 (,) 分隔)
-
解除 Win10 UWP 应用回环代理限制
@@ -862,7 +859,7 @@
规则详细说明文档
- 支持填写 DnsObject,JSON 格式,点击查看文档
+ 请填写 DnsObject,JSON 格式,点击查看文档
普通分组此处请留空
@@ -1590,9 +1587,6 @@
EchConfigList
-
- EchForceQuery
-
完整证书(链),PEM 格式
@@ -1716,4 +1710,10 @@
仅用于抓取自签证书,存在中间人风险。
-
\ No newline at end of file
+
+ 测试 UDP 延迟 (多选)
+
+
+ UDP 测试地址
+
+
diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx
index 90d3b549..17ee910a 100644
--- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx
+++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx
@@ -121,7 +121,7 @@
匯出分享連結至剪貼簿成功
- 請先檢查設定
+ 配置項目無效,請檢查或重新選擇
設定格式不正確
@@ -720,9 +720,6 @@
認證密碼
-
- 自訂 DNS (可多個,用逗號 (,) 分隔)
-
解除 Win10 UWP 應用回環代理限制
@@ -862,7 +859,7 @@
規則詳細說明檔案
- 支援填寫 DnsObject,JSON 格式,點擊查看說明
+ 請填寫 DnsObject,JSON 格式,點擊查看說明
普通分組此處請留空
@@ -1581,9 +1578,6 @@
EchConfigList
-
- EchForceQuery
-
完整憑證(鏈),PEM 格式
@@ -1710,4 +1704,10 @@
僅用於抓取自簽證書,存在中間人風險。
+
+ Test Configurations UDP Delay
+
+
+ UDP Test Url
+
\ No newline at end of file
diff --git a/v2rayN/ServiceLib/ServiceLib.csproj b/v2rayN/ServiceLib/ServiceLib.csproj
index 169c1080..60d9c622 100644
--- a/v2rayN/ServiceLib/ServiceLib.csproj
+++ b/v2rayN/ServiceLib/ServiceLib.csproj
@@ -84,4 +84,8 @@
+
+
+
+
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs
index a0d172ba..f5a7da4b 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxDnsService.cs
@@ -298,7 +298,7 @@ public partial class CoreConfigSingboxService
var rules = JsonUtils.Deserialize>(routing.RuleSet) ?? [];
var expectedIPCidr = new List();
var expectedIPsRegions = new List();
- var regionNames = new HashSet();
+ var regionName = string.Empty;
if (!string.IsNullOrEmpty(simpleDnsItem?.DirectExpectedIPs))
{
@@ -310,16 +310,16 @@ public partial class CoreConfigSingboxService
foreach (var ip in ipItems)
{
- if (ip.StartsWith("geoip:", StringComparison.OrdinalIgnoreCase))
+ if (ip.StartsWith(Global.GeoIPPrefix, StringComparison.OrdinalIgnoreCase))
{
- var region = ip["geoip:".Length..];
- if (!string.IsNullOrEmpty(region))
+ var region = ip[Global.GeoIPPrefix.Length..];
+ if (string.IsNullOrEmpty(region))
{
- expectedIPsRegions.Add(region);
- regionNames.Add(region);
- regionNames.Add($"geolocation-{region}");
- regionNames.Add($"tld-{region}");
+ continue;
}
+
+ expectedIPsRegions.Add(region);
+ regionName = region;
}
else
{
@@ -352,19 +352,25 @@ public partial class CoreConfigSingboxService
rule.server = Global.SingboxDirectDNSTag;
rule.strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom);
- if (expectedIPsRegions.Count > 0 && rule.geosite?.Count > 0)
+ if (expectedIPsRegions.Count > 0 && rule.geosite?.Count > 0 && !regionName.IsNullOrEmpty())
{
- var geositeSet = new HashSet(rule.geosite);
- if (regionNames.Intersect(geositeSet).Any())
+ var regionGeosite = rule.geosite.Where(g => g.EndsWith($"-{regionName}", StringComparison.OrdinalIgnoreCase)
+ || g.EndsWith($"@{regionName}", StringComparison.OrdinalIgnoreCase)
+ || g == regionName).ToList();
+ if (regionGeosite.Count > 0)
{
+ rule.geosite.RemoveAll(regionGeosite.Contains);
+ var rule4ExpectedIPs = JsonUtils.DeepCopy(rule);
+ rule4ExpectedIPs.geosite = regionGeosite;
if (expectedIPsRegions.Count > 0)
{
- rule.geoip = expectedIPsRegions;
+ rule4ExpectedIPs.geoip = expectedIPsRegions;
}
if (expectedIPCidr.Count > 0)
{
- rule.ip_cidr = expectedIPCidr;
+ rule4ExpectedIPs.ip_cidr = expectedIPCidr;
}
+ _coreConfig.dns.rules.Add(rule4ExpectedIPs);
}
}
}
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs
index 5b35db01..c438dabd 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs
@@ -227,7 +227,7 @@ public partial class CoreConfigSingboxService
: _config.HysteriaItem.UpMbps;
int? downMbps = protocolExtra?.DownMbps is { } sd and >= 0
? sd
- : _config.HysteriaItem.UpMbps;
+ : _config.HysteriaItem.DownMbps;
outbound.up_mbps = upMbps > 0 ? upMbps : null;
outbound.down_mbps = downMbps > 0 ? downMbps : null;
var ports = protocolExtra?.Ports?.IsNullOrEmpty() == false ? protocolExtra.Ports : null;
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs
index 92ee56a2..2b6bdf2b 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs
@@ -329,11 +329,52 @@ public partial class CoreConfigSingboxService
if (item.Ip?.Count > 0)
{
var countIp = 0;
- foreach (var it in item.Ip)
+ var negativeIpList = item.Ip.Where(it => it.StartsWith('!')).ToList();
+ if (negativeIpList.Count > 0)
{
- if (ParseV2Address(it, rule2))
+ var positiveIpList = item.Ip.Except(negativeIpList).ToList();
+ var positiveRule = rule2;
+ positiveRule = JsonUtils.DeepCopy(rule2);
+ positiveRule.outbound = null;
+ positiveRule.action = null;
+ foreach (var it in positiveIpList)
{
- countIp++;
+ if (ParseV2Address(it, positiveRule))
+ {
+ countIp++;
+ }
+ }
+ var negativeRule = new Rule4Sbox();
+ foreach (var it in negativeIpList)
+ {
+ // Remove first '!' and trim spaces
+ var ip = it[1..].Trim();
+ if (ParseV2Address(ip, negativeRule))
+ {
+ countIp++;
+ }
+ }
+ negativeRule.invert = true;
+ rule2 = new Rule4Sbox()
+ {
+ outbound = rule2.outbound,
+ action = rule2.action,
+ type = "logical",
+ mode = "or",
+ rules = [
+ positiveRule,
+ negativeRule
+ ]
+ };
+ }
+ else
+ {
+ foreach (var it in item.Ip)
+ {
+ if (ParseV2Address(it, rule2))
+ {
+ countIp++;
+ }
}
}
if (countIp > 0)
@@ -406,10 +447,10 @@ public partial class CoreConfigSingboxService
{
return false;
}
- else if (domain.StartsWith("geosite:"))
+ else if (domain.StartsWith(Global.GeoSitePrefix))
{
rule.geosite ??= [];
- rule.geosite?.Add(domain.Substring(8));
+ rule.geosite?.Add(domain[Global.GeoSitePrefix.Length..]);
}
else if (domain.StartsWith("regexp:"))
{
@@ -450,28 +491,18 @@ public partial class CoreConfigSingboxService
{
return false;
}
- else if (address.Equals("geoip:private"))
+ else if (address.Equals($"{Global.GeoIPPrefix}private"))
{
rule.ip_is_private = true;
}
- else if (address.StartsWith("geoip:"))
+ else if (address.StartsWith(Global.GeoIPPrefix))
{
- rule.geoip ??= new();
- rule.geoip?.Add(address.Substring(6));
- }
- else if (address.Equals("geoip:!private"))
- {
- rule.ip_is_private = false;
- }
- else if (address.StartsWith("geoip:!"))
- {
- rule.geoip ??= new();
- rule.geoip?.Add(address.Substring(6));
- rule.invert = true;
+ rule.geoip ??= [];
+ rule.geoip?.Add(address[Global.GeoIPPrefix.Length..]);
}
else
{
- rule.ip_cidr ??= new();
+ rule.ip_cidr ??= [];
rule.ip_cidr?.Add(address);
}
return true;
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs
index 6a815d62..667f7818 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs
@@ -161,6 +161,11 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
listen = Global.Loopback,
port = port,
protocol = EInboundProtocol.mixed.ToString(),
+ settings = new Inboundsettings4Ray()
+ {
+ udp = true,
+ auth = "noauth"
+ },
};
inbound.tag = inbound.protocol + inbound.port.ToString();
_coreConfig.inbounds.Add(inbound);
@@ -256,6 +261,11 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
listen = Global.Loopback,
port = port,
protocol = EInboundProtocol.mixed.ToString(),
+ settings = new Inboundsettings4Ray()
+ {
+ udp = true,
+ auth = "noauth"
+ },
});
_coreConfig.routing.rules.Add(BuildFinalRule());
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs
index 7faae5b1..1777d16a 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs
@@ -121,7 +121,7 @@ public partial class CoreConfigV2rayService
var proxyGeositeList = new List();
var expectedDomainList = new List();
var expectedIPs = new List();
- var regionNames = new HashSet();
+ var regionName = string.Empty;
var bootstrapDNSAddress = ParseDnsAddresses(simpleDNSItem?.BootstrapDNS, Global.DomainPureIPDNSAddress.First());
var dnsServerDomains = new List();
@@ -160,18 +160,14 @@ public partial class CoreConfigV2rayService
.Where(s => !string.IsNullOrEmpty(s))
.ToList();
- foreach (var ip in expectedIPs)
+ foreach (var region in from ip in expectedIPs
+ where ip.StartsWith(Global.GeoIPPrefix, StringComparison.OrdinalIgnoreCase)
+ select ip[Global.GeoIPPrefix.Length..]
+ into region
+ where !string.IsNullOrEmpty(region)
+ select region)
{
- if (ip.StartsWith("geoip:", StringComparison.OrdinalIgnoreCase))
- {
- var region = ip["geoip:".Length..];
- if (!string.IsNullOrEmpty(region))
- {
- regionNames.Add($"geosite:{region}");
- regionNames.Add($"geosite:geolocation-{region}");
- regionNames.Add($"geosite:tld-{region}");
- }
- }
+ regionName = region;
}
}
@@ -201,9 +197,14 @@ public partial class CoreConfigV2rayService
if (item.OutboundTag == Global.DirectTag)
{
- if (normalizedDomain.StartsWith("geosite:") || normalizedDomain.StartsWith("ext:"))
+ if (normalizedDomain.StartsWith(Global.GeoSitePrefix) || normalizedDomain.StartsWith("ext:"))
{
- (regionNames.Contains(normalizedDomain) ? expectedDomainList : directGeositeList).Add(normalizedDomain);
+ var isExpectedDomain = !regionName.IsNullOrEmpty()
+ || normalizedDomain.EndsWith($"-{regionName}")
+ || normalizedDomain.EndsWith($"@{regionName}")
+ || normalizedDomain == Global.GeoSitePrefix + regionName;
+ var targetList = isExpectedDomain ? expectedDomainList : directGeositeList;
+ targetList.Add(normalizedDomain);
}
else
{
@@ -212,7 +213,7 @@ public partial class CoreConfigV2rayService
}
else if (item.OutboundTag != Global.BlockTag)
{
- if (normalizedDomain.StartsWith("geosite:") || normalizedDomain.StartsWith("ext:"))
+ if (normalizedDomain.StartsWith(Global.GeoSitePrefix) || normalizedDomain.StartsWith("ext:"))
{
proxyGeositeList.Add(normalizedDomain);
}
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs
index b058dfa1..aeae3618 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs
@@ -328,6 +328,7 @@ public partial class CoreConfigV2rayService
var host = string.Empty;
var path = string.Empty;
var kcpSeed = string.Empty;
+ var kcpMtu = 0;
var headerType = string.Empty;
var xhttpExtra = string.Empty;
switch (network)
@@ -341,6 +342,7 @@ public partial class CoreConfigV2rayService
case nameof(ETransport.kcp):
kcpSeed = transport.KcpSeed?.TrimEx() ?? string.Empty;
headerType = transport.KcpHeaderType?.TrimEx() ?? string.Empty;
+ kcpMtu = transport.KcpMtu > 0 ? transport.KcpMtu!.Value : _config.KcpItem.Mtu;
break;
case nameof(ETransport.ws):
@@ -381,7 +383,6 @@ public partial class CoreConfigV2rayService
alpn = _node.GetAlpn(),
fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint,
echConfigList = _node.EchConfigList.NullIfEmpty(),
- echForceQuery = _node.EchForceQuery.NullIfEmpty()
};
if (sni.IsNotEmpty())
{
@@ -391,6 +392,11 @@ public partial class CoreConfigV2rayService
{
tlsSettings.serverName = Utils.String2List(host)?.First();
}
+ if (!tlsSettings.echConfigList.IsNullOrEmpty())
+ {
+ // For legacy xray compatibility, remove this in the future
+ tlsSettings.echForceQuery = "full";
+ }
var certs = CertPemManager.ParsePemChain(_node.Cert);
if (certs.Count > 0)
{
@@ -441,7 +447,7 @@ public partial class CoreConfigV2rayService
case nameof(ETransport.kcp):
KcpSettings4Ray kcpSettings = new()
{
- mtu = _config.KcpItem.Mtu,
+ mtu = kcpMtu,
tti = _config.KcpItem.Tti
};
@@ -546,6 +552,7 @@ public partial class CoreConfigV2rayService
FillOutboundMux(outbound);
break;
+
case nameof(ETransport.grpc):
GrpcSettings4Ray grpcSettings = new()
{
@@ -569,7 +576,7 @@ public partial class CoreConfigV2rayService
: _config.HysteriaItem.UpMbps;
int? downMbps = protocolExtra?.DownMbps is { } sd and >= 0
? sd
- : _config.HysteriaItem.UpMbps;
+ : _config.HysteriaItem.DownMbps;
var hopInterval = !protocolExtra.HopInterval.IsNullOrEmpty()
? protocolExtra.HopInterval
: (_config.HysteriaItem.HopInterval >= 5
diff --git a/v2rayN/ServiceLib/Services/SpeedtestService.cs b/v2rayN/ServiceLib/Services/SpeedtestService.cs
index 362f64d0..251522ba 100644
--- a/v2rayN/ServiceLib/Services/SpeedtestService.cs
+++ b/v2rayN/ServiceLib/Services/SpeedtestService.cs
@@ -1,3 +1,5 @@
+using ServiceLib.UdpTest;
+
namespace ServiceLib.Services;
public class SpeedtestService(Config config, Func updateFunc)
@@ -49,6 +51,10 @@ public class SpeedtestService(Config config, Func updateF
await RunRealPingBatchAsync(lstSelected, exitLoopKey);
break;
+ case ESpeedActionType.UdpTest:
+ await RunUdpTestBatchAsync(lstSelected, exitLoopKey);
+ break;
+
case ESpeedActionType.Speedtest:
await RunMixedTestAsync(lstSelected, 1, true, exitLoopKey);
break;
@@ -101,6 +107,7 @@ public class SpeedtestService(Config config, Func updateF
{
case ESpeedActionType.Tcping:
case ESpeedActionType.Realping:
+ case ESpeedActionType.UdpTest:
await UpdateFunc(it.IndexId, ResUI.Speedtesting, "");
ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
break;
@@ -238,6 +245,86 @@ public class SpeedtestService(Config config, Func updateF
return true;
}
+ private async Task RunUdpTestBatchAsync(List lstSelected, string exitLoopKey, int pageSize = 0)
+ {
+ if (pageSize <= 0)
+ {
+ pageSize = lstSelected.Count < Global.SpeedTestPageSize ? lstSelected.Count : Global.SpeedTestPageSize;
+ }
+ var lstTest = GetTestBatchItem(lstSelected, pageSize);
+
+ List lstFailed = new();
+ foreach (var lst in lstTest)
+ {
+ var ret = await RunUdpTestAsync(lst, exitLoopKey);
+ if (ret == false)
+ {
+ lstFailed.AddRange(lst);
+ }
+ await Task.Delay(100);
+ }
+
+ //Retest the failed part
+ if (lstFailed.Count > 0)
+ {
+ if (ShouldStopTest(exitLoopKey))
+ {
+ await UpdateFunc("", ResUI.SpeedtestingSkip);
+ return;
+ }
+
+ await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
+
+ await RunUdpTestAsync(lstFailed, exitLoopKey);
+ }
+ }
+
+ private async Task RunUdpTestAsync(List selecteds, string exitLoopKey)
+ {
+ ProcessService processService = null;
+ try
+ {
+ processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds);
+ if (processService is null)
+ {
+ return false;
+ }
+ await Task.Delay(1000);
+
+ List tasks = new();
+ foreach (var it in selecteds)
+ {
+ if (!it.AllowTest)
+ {
+ continue;
+ }
+
+ if (ShouldStopTest(exitLoopKey))
+ {
+ return false;
+ }
+
+ tasks.Add(Task.Run(async () =>
+ {
+ await DoUdpTest(it);
+ }));
+ }
+ await Task.WhenAll(tasks);
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(_tag, ex);
+ }
+ finally
+ {
+ if (processService != null)
+ {
+ await processService?.StopAsync();
+ }
+ }
+ return true;
+ }
+
private async Task RunMixedTestAsync(List selecteds, int concurrencyCount, bool blSpeedTest, string exitLoopKey)
{
using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount);
@@ -330,6 +417,24 @@ public class SpeedtestService(Config config, Func updateF
});
}
+ private async Task DoUdpTest(ServerTestItem it)
+ {
+ var udpService = UdpTestService.CreateFromTarget(_config?.SpeedTestItem.UdpTestTarget, out var udpTestUrl);
+ var responseTime = -1;
+ try
+ {
+ responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, TimeSpan.FromSeconds(5))).TotalMilliseconds;
+ }
+ catch
+ {
+ // ignored
+ }
+
+ ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
+ await UpdateFunc(it.IndexId, responseTime.ToString());
+ return responseTime;
+ }
+
private async Task GetTcpingTime(string url, int port)
{
var responseTime = -1;
diff --git a/v2rayN/ServiceLib/Services/UpdateService.cs b/v2rayN/ServiceLib/Services/UpdateService.cs
index 86b5d889..57f9d14b 100644
--- a/v2rayN/ServiceLib/Services/UpdateService.cs
+++ b/v2rayN/ServiceLib/Services/UpdateService.cs
@@ -299,7 +299,6 @@ public class UpdateService(Config config, Func updateFunc)
return url;
}
-
else if (Utils.IsLinux())
{
var arch = RuntimeInformation.ProcessArchitecture;
@@ -314,7 +313,6 @@ public class UpdateService(Config config, Func updateFunc)
_ => null,
};
}
-
else if (Utils.IsMacOS())
{
return RuntimeInformation.ProcessArchitecture switch
@@ -377,8 +375,8 @@ public class UpdateService(Config config, Func updateFunc)
var rules = JsonUtils.Deserialize>(routing.RuleSet);
foreach (var item in rules ?? [])
{
- AddPrefixedItems(item.Ip, "geoip:", geoipFiles);
- AddPrefixedItems(item.Domain, "geosite:", geoSiteFiles);
+ AddPrefixedItems(item.Ip, Global.GeoIPPrefix, geoipFiles);
+ AddPrefixedItems(item.Domain, Global.GeoSitePrefix, geoSiteFiles);
}
}
diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs
index 3dbea94c..20439a03 100644
--- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs
+++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs
@@ -106,6 +106,9 @@ public class AddServerViewModel : MyReactiveObject
[Reactive]
public string KcpSeed { get; set; }
+ [Reactive]
+ public int? KcpMtu { get; set; }
+
public string TransportHeaderType
{
get => SelectedSource.GetNetwork() switch
@@ -287,26 +290,26 @@ public class AddServerViewModel : MyReactiveObject
Cert = SelectedSource?.Cert?.ToString() ?? string.Empty;
CertSha = SelectedSource?.CertSha?.ToString() ?? string.Empty;
- var protocolExtra = SelectedSource?.GetProtocolExtra();
- var transport = SelectedSource?.GetTransportExtra();
- Ports = protocolExtra?.Ports ?? string.Empty;
- AlterId = int.TryParse(protocolExtra?.AlterId, out var result) ? result : 0;
- Flow = protocolExtra?.Flow ?? string.Empty;
- SalamanderPass = protocolExtra?.SalamanderPass ?? string.Empty;
- UpMbps = protocolExtra?.UpMbps;
- DownMbps = protocolExtra?.DownMbps;
- HopInterval = protocolExtra?.HopInterval ?? string.Empty;
- VmessSecurity = protocolExtra?.VmessSecurity?.IsNullOrEmpty() == false ? protocolExtra.VmessSecurity : Global.DefaultSecurity;
- VlessEncryption = protocolExtra?.VlessEncryption.IsNullOrEmpty() == false ? protocolExtra.VlessEncryption : Global.None;
- SsMethod = protocolExtra?.SsMethod ?? string.Empty;
- WgPublicKey = protocolExtra?.WgPublicKey ?? string.Empty;
- WgInterfaceAddress = protocolExtra?.WgInterfaceAddress ?? string.Empty;
- WgReserved = protocolExtra?.WgReserved ?? string.Empty;
- WgMtu = protocolExtra?.WgMtu ?? 1280;
- Uot = protocolExtra?.Uot ?? false;
- CongestionControl = protocolExtra?.CongestionControl ?? string.Empty;
- InsecureConcurrency = protocolExtra?.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null;
- NaiveQuic = protocolExtra?.NaiveQuic ?? false;
+ var protocolExtra = SelectedSource?.GetProtocolExtra() ?? new();
+ var transport = SelectedSource?.GetTransportExtra() ?? new();
+ Ports = protocolExtra.Ports ?? string.Empty;
+ AlterId = int.TryParse(protocolExtra.AlterId, out var result) ? result : 0;
+ Flow = protocolExtra.Flow ?? string.Empty;
+ SalamanderPass = protocolExtra.SalamanderPass ?? string.Empty;
+ UpMbps = protocolExtra.UpMbps;
+ DownMbps = protocolExtra.DownMbps;
+ HopInterval = protocolExtra.HopInterval ?? string.Empty;
+ VmessSecurity = protocolExtra.VmessSecurity?.IsNullOrEmpty() == false ? protocolExtra.VmessSecurity : Global.DefaultSecurity;
+ VlessEncryption = protocolExtra.VlessEncryption?.IsNullOrEmpty() == false ? protocolExtra.VlessEncryption : Global.None;
+ SsMethod = protocolExtra.SsMethod ?? string.Empty;
+ WgPublicKey = protocolExtra.WgPublicKey ?? string.Empty;
+ WgInterfaceAddress = protocolExtra.WgInterfaceAddress ?? string.Empty;
+ WgReserved = protocolExtra.WgReserved ?? string.Empty;
+ WgMtu = protocolExtra.WgMtu ?? 1280;
+ Uot = protocolExtra.Uot ?? false;
+ CongestionControl = protocolExtra.CongestionControl ?? string.Empty;
+ InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null;
+ NaiveQuic = protocolExtra.NaiveQuic ?? false;
RawHeaderType = transport.RawHeaderType ?? Global.None;
Host = transport.Host ?? string.Empty;
@@ -318,6 +321,7 @@ public class AddServerViewModel : MyReactiveObject
GrpcMode = transport.GrpcMode.IsNullOrEmpty() ? Global.GrpcGunMode : transport.GrpcMode;
KcpHeaderType = transport.KcpHeaderType.IsNullOrEmpty() ? Global.None : transport.KcpHeaderType;
KcpSeed = transport.KcpSeed ?? string.Empty;
+ KcpMtu = transport.KcpMtu;
}
private async Task SaveServerAsync()
@@ -381,6 +385,7 @@ public class AddServerViewModel : MyReactiveObject
GrpcMode = GrpcMode.NullIfEmpty(),
KcpHeaderType = KcpHeaderType.NullIfEmpty(),
KcpSeed = KcpSeed.NullIfEmpty(),
+ KcpMtu = KcpMtu > 0 ? KcpMtu : null,
};
SelectedSource.SetProtocolExtra(SelectedSource.GetProtocolExtra() with
diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs
index 1d26f02f..51db6809 100644
--- a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs
+++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs
@@ -59,6 +59,7 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public int SpeedTestTimeout { get; set; }
[Reactive] public string SpeedTestUrl { get; set; }
[Reactive] public string SpeedPingTestUrl { get; set; }
+ [Reactive] public string UdpTestTarget { get; set; }
[Reactive] public int MixedConcurrencyCount { get; set; }
[Reactive] public bool EnableHWA { get; set; }
[Reactive] public string SubConvertUrl { get; set; }
@@ -195,6 +196,7 @@ public class OptionSettingViewModel : MyReactiveObject
SpeedTestUrl = _config.SpeedTestItem.SpeedTestUrl;
MixedConcurrencyCount = _config.SpeedTestItem.MixedConcurrencyCount;
SpeedPingTestUrl = _config.SpeedTestItem.SpeedPingTestUrl;
+ UdpTestTarget = _config.SpeedTestItem.UdpTestTarget;
EnableHWA = _config.GuiItem.EnableHWA;
SubConvertUrl = _config.ConstItem.SubConvertUrl;
MainGirdOrientation = (int)_config.UiItem.MainGirdOrientation;
@@ -368,6 +370,7 @@ public class OptionSettingViewModel : MyReactiveObject
_config.SpeedTestItem.MixedConcurrencyCount = MixedConcurrencyCount;
_config.SpeedTestItem.SpeedTestUrl = SpeedTestUrl;
_config.SpeedTestItem.SpeedPingTestUrl = SpeedPingTestUrl;
+ _config.SpeedTestItem.UdpTestTarget = UdpTestTarget;
_config.GuiItem.EnableHWA = EnableHWA;
_config.ConstItem.SubConvertUrl = SubConvertUrl;
_config.UiItem.MainGirdOrientation = (EGirdOrientation)MainGirdOrientation;
diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs
index f44f52e3..88e8b679 100644
--- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs
+++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs
@@ -60,6 +60,7 @@ public class ProfilesViewModel : MyReactiveObject
public ReactiveCommand TcpingServerCmd { get; }
public ReactiveCommand RealPingServerCmd { get; }
+ public ReactiveCommand UdpTestServerCmd { get; }
public ReactiveCommand SpeedServerCmd { get; }
public ReactiveCommand SortServerResultCmd { get; }
public ReactiveCommand RemoveInvalidServerResultCmd { get; }
@@ -178,6 +179,10 @@ public class ProfilesViewModel : MyReactiveObject
{
await ServerSpeedtest(ESpeedActionType.Realping);
}, canEditRemove);
+ UdpTestServerCmd = ReactiveCommand.CreateFromTask(async () =>
+ {
+ await ServerSpeedtest(ESpeedActionType.UdpTest);
+ }, canEditRemove);
SpeedServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await ServerSpeedtest(ESpeedActionType.Speedtest);
diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml
index f29afb7f..3ab748b1 100644
--- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml
+++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml
@@ -306,7 +306,7 @@
x:Name="txtSecurity5"
Grid.Row="3"
Grid.Column="1"
- Width="200"
+ Width="400"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
@@ -831,7 +831,7 @@
x:Name="gridTransportKcp"
ColumnDefinitions="300,Auto"
IsVisible="False"
- RowDefinitions="Auto,Auto">
+ RowDefinitions="Auto,Auto,Auto">
+ Margin="{StaticResource Margin4}"
+ HorizontalAlignment="Left" />
+
+
+ Margin="{StaticResource Margin4}"
+ HorizontalAlignment="Left" />
-
-
-
cmbFingerprint2.ItemsSource = Global.Fingerprints;
cmbAllowInsecure.ItemsSource = Global.AllowInsecure;
cmbAlpn.ItemsSource = Global.Alpns;
- cmbEchForceQuery.ItemsSource = Global.EchForceQuerys;
- var lstStreamSecurity = new List();
- lstStreamSecurity.Add(string.Empty);
- lstStreamSecurity.Add(Global.StreamSecurity);
+ var lstStreamSecurity = new List { string.Empty, Global.StreamSecurity };
switch (profileItem.ConfigType)
{
@@ -79,7 +76,7 @@ public partial class AddServerWindow : WindowBase
sepa2.IsVisible = false;
gridTransport.IsVisible = false;
cmbFingerprint.IsEnabled = false;
- cmbFingerprint.SelectedValue = string.Empty;
+ cmbAlpn.IsEnabled = false;
break;
case EConfigType.TUIC:
@@ -88,7 +85,6 @@ public partial class AddServerWindow : WindowBase
gridTransport.IsVisible = false;
cmbCoreType.IsEnabled = false;
cmbFingerprint.IsEnabled = false;
- cmbFingerprint.SelectedValue = string.Empty;
gridFinalmask.IsVisible = false;
cmbCongestionControl8.ItemsSource = Global.TuicCongestionControls;
@@ -119,11 +115,8 @@ public partial class AddServerWindow : WindowBase
cmbCoreType.IsEnabled = false;
gridFinalmask.IsVisible = false;
cmbFingerprint.IsEnabled = false;
- cmbFingerprint.SelectedValue = string.Empty;
cmbAlpn.IsEnabled = false;
- cmbAlpn.SelectedValue = string.Empty;
cmbAllowInsecure.IsEnabled = false;
- cmbAllowInsecure.SelectedValue = string.Empty;
cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls;
break;
@@ -218,6 +211,7 @@ public partial class AddServerWindow : WindowBase
this.Bind(ViewModel, vm => vm.KcpHeaderType, v => v.cmbHeaderTypeKcp.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.KcpSeed, v => v.txtKcpSeed.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.KcpMtu, v => v.txtKcpMtu.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostWs.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Path, v => v.txtPathWs.Text).DisposeWith(disposables);
@@ -245,7 +239,6 @@ public partial class AddServerWindow : WindowBase
this.Bind(ViewModel, vm => vm.AllowInsecureCertFetch, v => v.togAllowInsecureCertFetch.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AllowInsecureCertFetch, v => v.txtAllowInsecureCertFetchTips.IsVisible).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables);
- this.Bind(ViewModel, vm => vm.SelectedSource.EchForceQuery, v => v.cmbEchForceQuery.SelectedValue).DisposeWith(disposables);
//reality
this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables);
@@ -337,21 +330,27 @@ public partial class AddServerWindow : WindowBase
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;
diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml
index 1b2a691c..b736de32 100644
--- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml
+++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml
@@ -342,11 +342,6 @@
-
-
@@ -494,7 +489,6 @@
Header="{x:Static resx:ResUI.TbSettingsTunMode}">
-
diff --git a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml
index 640a4f6f..50c938fa 100644
--- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml
+++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml
@@ -355,7 +355,7 @@
+ RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
-
+ HorizontalAlignment="Left" />-->
+
+
+
-
diff --git a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs
index 6501468d..ab58081d 100644
--- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs
+++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml.cs
@@ -49,6 +49,7 @@ public partial class OptionSettingWindow : WindowBase
cmbSpeedTestTimeout.ItemsSource = Enumerable.Range(2, 5).Select(i => i * 5).ToList();
cmbSpeedTestUrl.ItemsSource = Global.SpeedTestUrls;
cmbSpeedPingTestUrl.ItemsSource = Global.SpeedPingTestUrls;
+ cmbUdpTestTarget.ItemsSource = Global.UdpTestTargets;
cmbSubConvertUrl.ItemsSource = Global.SubConvertUrls;
cmbGetFilesSourceUrl.ItemsSource = Global.GeoFilesSources;
cmbSrsFilesSourceUrl.ItemsSource = Global.SingboxRulesetSources;
@@ -87,7 +88,7 @@ public partial class OptionSettingWindow : WindowBase
this.Bind(ViewModel, vm => vm.EnableStatistics, v => v.togEnableStatistics.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.DisplayRealTimeSpeed, v => v.togDisplayRealTimeSpeed.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.KeepOlderDedupl, v => v.togKeepOlderDedupl.IsChecked).DisposeWith(disposables);
- this.Bind(ViewModel, vm => vm.EnableAutoAdjustMainLvColWidth, v => v.togEnableAutoAdjustMainLvColWidth.IsChecked).DisposeWith(disposables);
+ //this.Bind(ViewModel, vm => vm.EnableAutoAdjustMainLvColWidth, v => v.togEnableAutoAdjustMainLvColWidth.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoHideStartup, v => v.togAutoHideStartup.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Hide2TrayWhenClose, v => v.togHide2TrayWhenClose.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.MacOSShowInDock, v => v.togMacOSShowInDock.IsChecked).DisposeWith(disposables);
@@ -97,6 +98,7 @@ public partial class OptionSettingWindow : WindowBase
this.Bind(ViewModel, vm => vm.SpeedTestTimeout, v => v.cmbSpeedTestTimeout.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SpeedTestUrl, v => v.cmbSpeedTestUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SpeedPingTestUrl, v => v.cmbSpeedPingTestUrl.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.UdpTestTarget, v => v.cmbUdpTestTarget.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.MixedConcurrencyCount, v => v.cmbMixedConcurrencyCount.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SubConvertUrl, v => v.cmbSubConvertUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel,
diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml
index e0751ca1..42fab7c2 100644
--- a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml
+++ b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml
@@ -140,6 +140,7 @@
x:Name="menuSpeedServer"
Header="{x:Static resx:ResUI.menuSpeedServer}"
InputGesture="Ctrl+T" />
+
-
+ Application.Current?.Dispatcher.Invoke(() =>
{
ShowHideWindow(true);
- }));
+ });
}
private async Task DelegateSnackMsg(string content)
diff --git a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
index 5a545c65..20339b4d 100644
--- a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
+++ b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
@@ -569,6 +569,8 @@
+
+
@@ -834,10 +836,26 @@
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
+ Text="{x:Static resx:ResUI.TbSettingsUdpTestUrl}" />
+
+
+
i * 5).ToList();
cmbSpeedTestUrl.ItemsSource = Global.SpeedTestUrls;
cmbSpeedPingTestUrl.ItemsSource = Global.SpeedPingTestUrls;
+ cmbUdpTestTarget.ItemsSource = Global.UdpTestTargets;
cmbSubConvertUrl.ItemsSource = Global.SubConvertUrls;
cmbGetFilesSourceUrl.ItemsSource = Global.GeoFilesSources;
cmbSrsFilesSourceUrl.ItemsSource = Global.SingboxRulesetSources;
@@ -102,6 +103,7 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.SpeedTestTimeout, v => v.cmbSpeedTestTimeout.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SpeedTestUrl, v => v.cmbSpeedTestUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SpeedPingTestUrl, v => v.cmbSpeedPingTestUrl.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.UdpTestTarget, v => v.cmbUdpTestTarget.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.MixedConcurrencyCount, v => v.cmbMixedConcurrencyCount.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EnableHWA, v => v.togEnableHWA.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SubConvertUrl, v => v.cmbSubConvertUrl.Text).DisposeWith(disposables);
diff --git a/v2rayN/v2rayN/Views/ProfilesView.xaml b/v2rayN/v2rayN/Views/ProfilesView.xaml
index fd984306..97d3af3f 100644
--- a/v2rayN/v2rayN/Views/ProfilesView.xaml
+++ b/v2rayN/v2rayN/Views/ProfilesView.xaml
@@ -158,6 +158,10 @@
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuRealPingServer}"
InputGestureText="Ctrl+R" />
+