diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..9364283f
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,29 @@
+name: Code Test
+
+on:
+ pull_request:
+ branches:
+ - master
+ paths:
+ - 'v2rayN/ServiceLib/Services/CoreConfig/**'
+ - 'v2rayN/ServiceLib/Handler/Fmt/**'
+ - '.github/workflows/test.yml'
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6.0.2
+ with:
+ submodules: 'recursive'
+ fetch-depth: '0'
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v5.2.0
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Test Code
+ working-directory: ./v2rayN
+ run: dotnet test ./ServiceLib.Tests
diff --git a/v2rayN/Directory.Build.props b/v2rayN/Directory.Build.props
index da1004ef..cca05716 100644
--- a/v2rayN/Directory.Build.props
+++ b/v2rayN/Directory.Build.props
@@ -1,7 +1,7 @@
- 7.21.0
+ 7.21.2
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/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs
index 91f2884b..158ba239 100644
--- a/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs
+++ b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs
@@ -1,6 +1,7 @@
using AwesomeAssertions;
using ServiceLib.Common;
using ServiceLib.Enums;
+using ServiceLib.Manager;
using ServiceLib.Models;
using ServiceLib.Services.CoreConfig;
using Xunit;
@@ -28,6 +29,54 @@ public class CoreConfigSingboxServiceTests
singboxConfig.inbounds.Should().Contain(i => i.type == nameof(EInboundProtocol.mixed));
}
+ [Fact]
+ public void GenerateClientConfigContent_TunWithLoopbackPreSocks_ShouldKeepMixedInbound()
+ {
+ var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
+ CoreConfigTestFactory.BindAppManagerConfig(config);
+ var node = CoreConfigTestFactory.CreateSocksNode(ECoreType.sing_box);
+ node.Address = Global.Loopback;
+ node.Port = 1080;
+ var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
+ {
+ IsTunEnabled = true,
+ };
+
+ var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
+
+ result.Success.Should().BeTrue($"ret msg: {result.Msg}");
+ var cfg = JsonUtils.Deserialize(result.Data!.ToString())!;
+
+ cfg.inbounds.Should().Contain(i =>
+ i.type == nameof(EInboundProtocol.mixed)
+ && i.listen == Global.Loopback
+ && i.listen_port == AppManager.Instance.GetLocalPort(EInboundProtocol.socks));
+ cfg.inbounds.Should().Contain(i => i.type == "tun");
+ }
+
+ [Fact]
+ public void GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface()
+ {
+ var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
+ config.CoreBasicItem.BindInterface = "eth0";
+ CoreConfigTestFactory.BindAppManagerConfig(config);
+
+ var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box);
+ var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
+ {
+ IsTunEnabled = true,
+ };
+
+ var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
+
+ result.Success.Should().BeTrue($"ret msg: {result.Msg}");
+ var cfg = JsonUtils.Deserialize(result.Data!.ToString())!;
+ var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
+
+ proxy.bind_interface.Should().Be("eth0");
+ proxy.detour.Should().BeNullOrEmpty();
+ }
+
[Fact]
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildSelector()
{
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.Tests/Fmt/InnerFmtTests.cs b/v2rayN/ServiceLib.Tests/Fmt/InnerFmtTests.cs
new file mode 100644
index 00000000..91df67f4
--- /dev/null
+++ b/v2rayN/ServiceLib.Tests/Fmt/InnerFmtTests.cs
@@ -0,0 +1,37 @@
+using AwesomeAssertions;
+using ServiceLib.Enums;
+using ServiceLib.Handler.Fmt;
+using ServiceLib.Tests.CoreConfig;
+using Xunit;
+
+namespace ServiceLib.Tests.Fmt;
+
+public class InnerFmtTests
+{
+ [Fact]
+ public void ToUriAndResolve_ShouldRoundTripPolicyGroupReferences()
+ {
+ var childA = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, "child-a", "child-a");
+ var childB = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "child-b", "child-b");
+ var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, "group-1", "group-1",
+ [childA.IndexId, childB.IndexId]);
+ group.SetProtocolExtra(group.GetProtocolExtra() with { SubChildItems = "original-sub" });
+
+ var uri = InnerFmt.ToUri([group, childA, childB]);
+
+ uri.Should().NotBeNullOrWhiteSpace();
+
+ var resolved = InnerFmt.Resolve(uri!, "sub-123");
+
+ resolved.Should().NotBeNull();
+ resolved.Should().HaveCount(3);
+
+ var resolvedGroup = resolved!.Single(x => x.Remarks == group.Remarks);
+ var resolvedChildA = resolved.Single(x => x.Remarks == childA.Remarks);
+ var resolvedChildB = resolved.Single(x => x.Remarks == childB.Remarks);
+
+ resolvedGroup.ConfigType.Should().Be(EConfigType.PolicyGroup);
+ resolvedGroup.GetProtocolExtra().SubChildItems.Should().Be("sub-123");
+ resolvedGroup.GetProtocolExtra().ChildItems.Should().Be($"{resolvedChildA.IndexId},{resolvedChildB.IndexId}");
+ }
+}
diff --git a/v2rayN/ServiceLib.Tests/Fmt/WireguardFmtTests.cs b/v2rayN/ServiceLib.Tests/Fmt/WireguardFmtTests.cs
new file mode 100644
index 00000000..717fa1bc
--- /dev/null
+++ b/v2rayN/ServiceLib.Tests/Fmt/WireguardFmtTests.cs
@@ -0,0 +1,47 @@
+using AwesomeAssertions;
+using ServiceLib.Handler.Fmt;
+using Xunit;
+
+namespace ServiceLib.Tests.Fmt;
+
+public class WireguardFmtTests
+{
+ [Fact]
+ public void ResolveConfig_ShouldParsePeersAndIgnoreInlineComments()
+ {
+ const string config =
+ """
+ [Interface]
+ PrivateKey = interface-private-key
+ Address = 10.0.0.2/32, fd00::2/128 ; inline comment
+ MTU = 1420
+
+ [Peer]
+ PublicKey = peer-public-key
+ PresharedKey = peer-preshared-key
+ Reserved = 1, 2, 3 # inline comment
+ Endpoint = [2001:db8::1]:51820 # inline comment
+
+ [Peer]
+ PublicKey = peer-public-key-2
+ Endpoint = example.com:12345
+ """;
+
+ var resolved = WireguardFmt.ResolveConfig(config);
+
+ resolved.Should().NotBeNull();
+ resolved.Should().HaveCount(2);
+
+ var first = resolved![0];
+ first.Address.Should().Be("2001:db8::1");
+ first.Port.Should().Be(51820);
+ first.Password.Should().Be("interface-private-key");
+ first.GetProtocolExtra().WgReserved.Should().Be("1, 2, 3");
+ first.GetProtocolExtra().WgInterfaceAddress.Should().Be("10.0.0.2/32, fd00::2/128");
+ first.GetProtocolExtra().WgMtu.Should().Be(1420);
+
+ var second = resolved[1];
+ second.Address.Should().Be("example.com");
+ second.Port.Should().Be(12345);
+ }
+}
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/Common/JsonUtils.cs b/v2rayN/ServiceLib/Common/JsonUtils.cs
index 7e2b7f78..951cdbd7 100644
--- a/v2rayN/ServiceLib/Common/JsonUtils.cs
+++ b/v2rayN/ServiceLib/Common/JsonUtils.cs
@@ -17,6 +17,13 @@ public class JsonUtils
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
+ private static readonly JsonSerializerOptions _defaultSerializeNoIndentedOptions = new()
+ {
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+
private static readonly JsonSerializerOptions _nullValueSerializeOptions = new()
{
WriteIndented = true,
@@ -24,6 +31,13 @@ public class JsonUtils
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
+ private static readonly JsonSerializerOptions _nullValueSerializeNoIndentedOptions = new()
+ {
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.Never,
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+
private static readonly JsonDocumentOptions _defaultDocumentOptions = new()
{
CommentHandling = JsonCommentHandling.Skip
@@ -104,7 +118,13 @@ public class JsonUtils
{
return result;
}
- var options = nullValue ? _nullValueSerializeOptions : _defaultSerializeOptions;
+ var options = (nullValue, indented) switch
+ {
+ (true, true) => _nullValueSerializeOptions,
+ (true, false) => _nullValueSerializeNoIndentedOptions,
+ (false, true) => _defaultSerializeOptions,
+ _ => _defaultSerializeNoIndentedOptions
+ };
result = JsonSerializer.Serialize(obj, options);
}
catch (Exception ex)
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..ca167fa3 100644
--- a/v2rayN/ServiceLib/Global.cs
+++ b/v2rayN/ServiceLib/Global.cs
@@ -64,9 +64,12 @@ public class Global
public const string HttpsProtocol = "https://";
public const string SocksProtocol = "socks://";
public const string Socks5Protocol = "socks5://";
+ public const string InnerUriProtocol = "v2rayn://";
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 +643,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 +694,6 @@ public class Global
""
];
- public static readonly List EchForceQuerys =
- [
- "none",
- "half",
- "full",
- ""
- ];
-
public static readonly List TunIcmpRoutingPolicies =
[
"rule",
diff --git a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs
index 886a1971..eb1af21b 100644
--- a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs
+++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs
@@ -47,6 +47,8 @@ public class CoreConfigContextBuilder
ProtectDomainList = [],
RawDnsItem = await AppManager.Instance.GetDNSItem(coreType),
RoutingItem = await ConfigHandler.GetDefaultRouting(config),
+ IsWindows = Utils.IsWindows(),
+ IsMacOS = Utils.IsMacOS(),
};
var validatorResult = NodeValidatorResult.Empty();
var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node);
diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
index 281db07d..54a0108a 100644
--- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs
+++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs
@@ -41,7 +41,6 @@ public static class ConfigHandler
Loglevel = "warning",
MuxEnabled = false,
};
- config.CoreBasicItem.SendThrough = config.CoreBasicItem.SendThrough?.TrimEx();
if (config.Inbound == null)
{
@@ -135,6 +134,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 +255,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 +704,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 +749,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))
@@ -785,12 +790,30 @@ public static class ConfigHandler
profileItem.Address = profileItem.Address.TrimEx();
profileItem.Password = profileItem.Password.TrimEx();
+ var wgReserved = profileItem.GetProtocolExtra().WgReserved?.TrimEx();
+ if (!wgReserved.IsNullOrEmpty()
+ && !wgReserved.Contains(','))
+ {
+ // Base64 format, convert to standard format
+ try
+ {
+ var bytes = Convert.FromBase64String(wgReserved);
+ var reserved = new byte[3];
+ Array.Copy(bytes, reserved, Math.Min(bytes.Length, 3));
+
+ wgReserved = string.Join(", ", reserved);
+ }
+ catch
+ {
+ // If conversion fails, keep the original value
+ }
+ }
profileItem.SetProtocolExtra(profileItem.GetProtocolExtra() with
{
WgPublicKey = profileItem.GetProtocolExtra().WgPublicKey?.TrimEx(),
WgPresharedKey = profileItem.GetProtocolExtra().WgPresharedKey?.TrimEx(),
WgInterfaceAddress = profileItem.GetProtocolExtra().WgInterfaceAddress?.TrimEx(),
- WgReserved = profileItem.GetProtocolExtra().WgReserved?.TrimEx(),
+ WgReserved = wgReserved,
WgMtu = profileItem.GetProtocolExtra().WgMtu is null or <= 0 ? Global.TunMtus.First() : profileItem.GetProtocolExtra().WgMtu,
});
@@ -848,8 +871,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 +1062,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);
@@ -1497,10 +1528,8 @@ public static class ConfigHandler
}
var subFilter = string.Empty;
- //remove sub items
if (isSub && subid.IsNotEmpty())
{
- await RemoveServersViaSubid(config, subid, isSub);
subFilter = (await AppManager.Instance.GetSubItem(subid))?.Filter ?? "";
}
@@ -1603,10 +1632,6 @@ public static class ConfigHandler
}
if (lstProfiles != null && lstProfiles.Count > 0)
{
- if (isSub && subid.IsNotEmpty())
- {
- await RemoveServersViaSubid(config, subid, isSub);
- }
var count = 0;
foreach (var it in lstProfiles)
{
@@ -1626,40 +1651,23 @@ public static class ConfigHandler
ProfileItem? profileItem = null;
//Is sing-box configuration
- if (profileItem is null)
- {
- profileItem = SingboxFmt.ResolveFull(strData, subRemarks);
- }
+ profileItem ??= SingboxFmt.ResolveFull(strData, subRemarks);
//Is v2ray configuration
- if (profileItem is null)
- {
- profileItem = V2rayFmt.ResolveFull(strData, subRemarks);
- }
+ profileItem ??= V2rayFmt.ResolveFull(strData, subRemarks);
//Is Html Page
if (profileItem is null && HtmlPageFmt.IsHtmlPage(strData))
{
return -1;
}
//Is Clash configuration
- if (profileItem is null)
- {
- profileItem = ClashFmt.ResolveFull(strData, subRemarks);
- }
+ profileItem ??= ClashFmt.ResolveFull(strData, subRemarks);
//Is hysteria configuration
- if (profileItem is null)
- {
- profileItem = Hysteria2Fmt.ResolveFull2(strData, subRemarks);
- }
+ profileItem ??= Hysteria2Fmt.ResolveFull2(strData, subRemarks);
if (profileItem is null || profileItem.Address.IsNullOrEmpty())
{
return -1;
}
- if (isSub && subid.IsNotEmpty())
- {
- await RemoveServersViaSubid(config, subid, isSub);
- }
-
profileItem.Subid = subid;
profileItem.IsSub = isSub;
profileItem.PreSocksPort = preSocksPort;
@@ -1689,11 +1697,6 @@ public static class ConfigHandler
return -1;
}
- if (isSub && subid.IsNotEmpty())
- {
- await RemoveServersViaSubid(config, subid, isSub);
- }
-
var lstSsServer = ShadowsocksFmt.ResolveSip008(strData);
if (lstSsServer?.Count > 0)
{
@@ -1714,6 +1717,86 @@ public static class ConfigHandler
return -1;
}
+ private static async Task AddBatchServers4Wireguard(Config config, string strData, string subid, bool isSub)
+ {
+ if (strData.IsNullOrEmpty())
+ {
+ return -1;
+ }
+ if (!(strData.Contains("[Interface]", StringComparison.OrdinalIgnoreCase)
+ && strData.Contains("[Peer]", StringComparison.OrdinalIgnoreCase)))
+ {
+ return -1;
+ }
+ var lstServer = WireguardFmt.ResolveConfig(strData);
+ if (lstServer?.Count > 0)
+ {
+ var counter = 0;
+ foreach (var item in lstServer)
+ {
+ item.Subid = subid;
+ item.IsSub = isSub;
+ if (await AddWireguardServer(config, item) == 0)
+ {
+ counter++;
+ }
+ }
+ await SaveConfig(config);
+ return counter;
+ }
+ return -1;
+ }
+
+ private static async Task AddBatchServers4InnerUri(Config config, string strData, string subid, bool isSub)
+ {
+ if (strData.IsNullOrEmpty())
+ {
+ return -1;
+ }
+
+ var lstServer = InnerFmt.Resolve(strData, subid);
+ if (lstServer?.Count > 0)
+ {
+ var counter = 0;
+ List lstAdd = [];
+ foreach (var profileItem in lstServer)
+ {
+ profileItem.Subid = subid;
+ profileItem.IsSub = isSub;
+
+ var addStatus = profileItem.ConfigType switch
+ {
+ EConfigType.VMess => await AddVMessServer(config, profileItem, false),
+ EConfigType.Shadowsocks => await AddShadowsocksServer(config, profileItem, false),
+ EConfigType.HTTP => await AddHttpServer(config, profileItem, false),
+ EConfigType.SOCKS => await AddSocksServer(config, profileItem, false),
+ EConfigType.Trojan => await AddTrojanServer(config, profileItem, false),
+ EConfigType.VLESS => await AddVlessServer(config, profileItem, false),
+ EConfigType.Hysteria2 => await AddHysteria2Server(config, profileItem, false),
+ EConfigType.TUIC => await AddTuicServer(config, profileItem, false),
+ EConfigType.WireGuard => await AddWireguardServer(config, profileItem, false),
+ EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
+ EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
+ EConfigType.PolicyGroup or EConfigType.ProxyChain => await AddServerCommon(config, profileItem, false),
+ _ => -1,
+ };
+ if (addStatus == 0)
+ {
+ counter++;
+ lstAdd.Add(profileItem);
+ }
+ }
+ if (lstAdd.Count > 0)
+ {
+ await SQLiteHelper.Instance.InsertAllAsync(lstAdd);
+ }
+ await SaveConfig(config);
+ return counter;
+ }
+
+ return -1;
+ }
+
///
/// Main entry point for adding batch servers from various formats
/// Tries different parsing methods to import as many servers as possible
@@ -1733,6 +1816,7 @@ public static class ConfigHandler
ProfileItem? activeProfile = null;
if (isSub && subid.IsNotEmpty())
{
+ await RemoveServersViaSubid(config, subid, true);
lstOriSub = await AppManager.Instance.ProfileItems(subid);
activeProfile = lstOriSub?.FirstOrDefault(t => t.IndexId == config.IndexId);
}
@@ -1756,6 +1840,26 @@ public static class ConfigHandler
counter = await AddBatchServers4SsSIP008(config, strData, subid, isSub);
}
+ //maybe wireguard config
+ if (counter < 1)
+ {
+ counter = await AddBatchServers4Wireguard(config, strData, subid, isSub);
+ }
+
+ //May be standard uri mixed with internal uri
+ var innerUriCount = await AddBatchServers4InnerUri(config, strData, subid, isSub);
+ if (innerUriCount > 0)
+ {
+ if (counter > 0)
+ {
+ counter += innerUriCount;
+ }
+ else
+ {
+ counter = innerUriCount;
+ }
+ }
+
//maybe other sub
if (counter < 1)
{
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/Handler/Fmt/InnerFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/InnerFmt.cs
new file mode 100644
index 00000000..16420e74
--- /dev/null
+++ b/v2rayN/ServiceLib/Handler/Fmt/InnerFmt.cs
@@ -0,0 +1,299 @@
+namespace ServiceLib.Handler.Fmt;
+
+public class InnerFmt
+{
+ private static readonly Lazy SessionSalt = new(() => Utils.GetGuid(false));
+
+ public static List? Resolve(string strData, string subid)
+ {
+ var list = new List();
+ // Overwrite externally imported indexIds to avoid possible sources of attacks
+ var indexIdMap = new Dictionary();
+ using (var reader = new StringReader(strData))
+ {
+ while (reader.ReadLine() is { } line)
+ {
+ if (line.IsNullOrEmpty())
+ {
+ continue;
+ }
+ var trimmedLine = line.Trim();
+ if (!trimmedLine.StartsWith(Global.InnerUriProtocol, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+ var profileItem = ResolveSingle(trimmedLine);
+ if (profileItem is null)
+ {
+ continue;
+ }
+ if (profileItem.ConfigType == EConfigType.Custom)
+ {
+ // Unsupported, also to avoid possible sources of attacks, skip it
+ continue;
+ }
+ // overwrite indexId
+ var newIndexId = Utils.GetGuid(false);
+ if (!profileItem.IndexId.IsNullOrEmpty())
+ {
+ // Ignore duplicated indexId
+ indexIdMap[profileItem.IndexId] = newIndexId;
+ }
+ profileItem.IndexId = newIndexId;
+ list.Add(profileItem);
+ }
+ }
+ // For group-type profile items, also overwrite the ChildItems and ChildSubId
+ var emptyGroupProfileList = new List();
+ foreach (var item in list.Where(i => i.ConfigType.IsGroupType()))
+ {
+ var protocolExtra = item.GetProtocolExtra();
+ // Only allow "self" as a special value for SubChildItems to avoid possible sources of attacks,
+ // which means it will be replaced with the subid, otherwise set it to null
+ //if (!protocolExtra.SubChildItems.IsNullOrEmpty())
+ if (protocolExtra.SubChildItems == "self")
+ {
+ protocolExtra = protocolExtra with
+ {
+ SubChildItems = subid
+ };
+ }
+ else
+ {
+ protocolExtra = protocolExtra with
+ {
+ SubChildItems = null
+ };
+ }
+ if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
+ {
+ var newChildIndexIds = childIndexIds
+ .Select(id => indexIdMap.GetValueOrDefault(id, null))
+ .Where(id => !id.IsNullOrEmpty())
+ .ToList();
+ protocolExtra = protocolExtra with
+ {
+ ChildItems = Utils.List2String(newChildIndexIds)
+ };
+ }
+ else
+ {
+ protocolExtra = protocolExtra with
+ {
+ ChildItems = null
+ };
+ }
+ item.SetProtocolExtra(protocolExtra);
+ if (protocolExtra.SubChildItems.IsNullOrEmpty()
+ && protocolExtra.ChildItems.IsNullOrEmpty())
+ {
+ emptyGroupProfileList.Add(item);
+ }
+ }
+ // Remove empty group profile items
+ list.RemoveAll(emptyGroupProfileList.Contains);
+ return list;
+ }
+
+ public static string? ToUri(List items)
+ {
+ var sb = new StringBuilder();
+ foreach (var item in items)
+ {
+ if (item.ConfigType == EConfigType.Custom)
+ {
+ continue;
+ }
+ var itemClone = JsonUtils.DeepCopy(item);
+ if (itemClone is null)
+ {
+ continue;
+ }
+ // overwrite indexId
+ var originalIndexId = itemClone.IndexId;
+ var newIndexId = GetReproducibleExportId(originalIndexId);
+ itemClone.IndexId = newIndexId;
+ if (itemClone.ConfigType.IsGroupType())
+ {
+ var protocolExtra = itemClone.GetProtocolExtra();
+ if (!protocolExtra.SubChildItems.IsNullOrEmpty())
+ {
+ protocolExtra = protocolExtra with
+ {
+ SubChildItems = "self"
+ };
+ }
+ if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
+ {
+ var newChildIndexIds = childIndexIds
+ .Select(GetReproducibleExportId)
+ .Where(id => !id.IsNullOrEmpty())
+ .ToList();
+ protocolExtra = protocolExtra with
+ {
+ ChildItems = Utils.List2String(newChildIndexIds)
+ };
+ }
+ itemClone.SetProtocolExtra(protocolExtra);
+ }
+ var uri = ToUriSingle(itemClone);
+ if (!uri.IsNullOrEmpty())
+ {
+ sb.AppendLine(uri);
+ }
+ }
+ return sb.Length > 0 ? sb.ToString() : null;
+ }
+
+ private static ProfileItem? ResolveSingle(string str)
+ {
+ // format: v2rayn://vless/{url-safe base64 encoded_string}
+ var parsedUri = Utils.TryUri(str);
+ if (parsedUri is null)
+ {
+ return null;
+ }
+ var segment = parsedUri.AbsolutePath.TrimStart('/');
+ var decodedResult = Utils.Base64Decode(segment);
+ var jsonNode = JsonUtils.ParseJson(decodedResult);
+ if (jsonNode is not JsonObject jsonObj)
+ {
+ return null;
+ }
+ // flatten
+ // move jsonObj.ProtoExtraObj to jsonObj.ProtoExtra (string)
+ // move jsonObj.TransportExtraObj to jsonObj.TransportExtra (string)
+ if (jsonObj.TryGetPropertyValue("ProtoExtraObj", out var protoExtraNode)
+ && protoExtraNode is JsonObject protoExtraObj)
+ {
+ jsonObj["ProtoExtra"] = JsonUtils.Serialize(protoExtraObj, false);
+ jsonObj.Remove("ProtoExtraObj");
+ }
+ if (jsonObj.TryGetPropertyValue("TransportExtraObj", out var transportExtraNode)
+ && transportExtraNode is JsonObject transportExtraObj)
+ {
+ jsonObj["TransportExtra"] = JsonUtils.Serialize(transportExtraObj, false);
+ jsonObj.Remove("TransportExtraObj");
+ }
+ var profileItem = JsonUtils.Deserialize(JsonUtils.Serialize(jsonObj, false));
+ if (profileItem is null)
+ {
+ return null;
+ }
+ if (profileItem.ConfigVersion != 4)
+ {
+ return null;
+ }
+ // Check Enum.IsDefined
+ if (!Enum.IsDefined(typeof(EConfigType), profileItem.ConfigType))
+ {
+ return null;
+ }
+ if (profileItem.CoreType is not (null or ECoreType.Xray or ECoreType.sing_box))
+ {
+ return null;
+ }
+ var protocolExtra = profileItem.GetProtocolExtra();
+ var multipleLoad = protocolExtra.MultipleLoad;
+ if (multipleLoad is not null && !Enum.IsDefined(typeof(EMultipleLoad), multipleLoad))
+ {
+ return null;
+ }
+ return profileItem;
+ }
+
+ private static string? ToUriSingle(ProfileItem item)
+ {
+ var jsonNode = JsonUtils.ParseJson(JsonUtils.Serialize(item, false));
+ if (jsonNode is not JsonObject jsonObj)
+ {
+ return null;
+ }
+ // unflatten
+ // move jsonObj.ProtoExtra (string) to jsonObj.ProtoExtraObj
+ // move jsonObj.TransportExtra (string) to jsonObj.TransportExtraObj
+ if (jsonObj.TryGetPropertyValue("ProtoExtra", out var protoExtraNode)
+ && protoExtraNode is JsonValue protoExtraValue
+ && protoExtraValue.TryGetValue(out var protoExtraStr)
+ && !protoExtraStr.IsNullOrEmpty()
+ && JsonUtils.ParseJson(protoExtraStr) is JsonObject protoExtraObj)
+ {
+ jsonObj["ProtoExtraObj"] = protoExtraObj;
+ jsonObj.Remove("ProtoExtra");
+ }
+ if (jsonObj.TryGetPropertyValue("TransportExtra", out var transportExtraNode)
+ && transportExtraNode is JsonValue transportExtraValue
+ && transportExtraValue.TryGetValue(out var transportExtraStr)
+ && !transportExtraStr.IsNullOrEmpty()
+ && JsonUtils.ParseJson(transportExtraStr) is JsonObject transportExtraObj)
+ {
+ jsonObj["TransportExtraObj"] = transportExtraObj;
+ jsonObj.Remove("TransportExtra");
+ }
+ // Remove empty properties to reduce the length of the exported string
+ RemoveEmptyJson(jsonObj);
+ var jsonStr = JsonUtils.Serialize(jsonObj, false);
+ var encodedStr = Utils.Base64Encode(jsonStr).Replace('+', '-').Replace('/', '_').Replace("=", "");
+ return $"{Global.InnerUriProtocol}{item.ConfigType.ToString().ToLower()}/{encodedStr}";
+ }
+
+ private static string GetReproducibleExportId(string originalIndexId)
+ {
+ if (originalIndexId.IsNullOrEmpty())
+ {
+ return originalIndexId;
+ }
+
+ var hash = HashCode.Combine(SessionSalt.Value, originalIndexId) & 0x7FFFFFFF;
+ var bytes = BitConverter.GetBytes(hash);
+ return Convert.ToBase64String(bytes).Replace("=", "");
+ }
+
+ private static void RemoveEmptyJson(JsonNode? node)
+ {
+ // ReSharper disable once ConvertIfStatementToSwitchStatement
+ if (node is JsonObject jsonObject)
+ {
+ var propertiesToRemove = new List();
+
+ foreach (var property in jsonObject)
+ {
+ RemoveEmptyJson(property.Value);
+
+ if (IsEmpty(property.Value))
+ {
+ propertiesToRemove.Add(property.Key);
+ }
+ }
+
+ foreach (var key in propertiesToRemove)
+ {
+ jsonObject.Remove(key);
+ }
+ }
+ else if (node is JsonArray jsonArray)
+ {
+ for (var i = jsonArray.Count - 1; i >= 0; i--)
+ {
+ RemoveEmptyJson(jsonArray[i]);
+
+ if (IsEmpty(jsonArray[i]))
+ {
+ jsonArray.RemoveAt(i);
+ }
+ }
+ }
+ }
+
+ private static bool IsEmpty(JsonNode? node)
+ {
+ return node switch
+ {
+ null => true,
+ JsonValue value when value.TryGetValue(out var str) => string.IsNullOrEmpty(str),
+ JsonObject obj => obj.Count == 0,
+ JsonArray arr => arr.Count == 0,
+ _ => false
+ };
+ }
+}
diff --git a/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs
index 73cb6a85..b0de1068 100644
--- a/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs
+++ b/v2rayN/ServiceLib/Handler/Fmt/WireguardFmt.cs
@@ -27,9 +27,10 @@ public class WireguardFmt : BaseFmt
item.SetProtocolExtra(item.GetProtocolExtra() with
{
WgPublicKey = GetQueryDecoded(query, "publickey"),
+ WgPresharedKey = GetQueryDecoded(query, "presharedkey"),
WgReserved = GetQueryDecoded(query, "reserved"),
WgInterfaceAddress = GetQueryDecoded(query, "address"),
- WgMtu = int.TryParse(GetQueryDecoded(query, "mtu"), out var mtuVal) ? mtuVal : 1280,
+ WgMtu = int.TryParse(GetQueryDecoded(query, "mtu"), out var mtuVal) ? mtuVal : null,
});
return item;
@@ -48,20 +49,183 @@ public class WireguardFmt : BaseFmt
remark = "#" + Utils.UrlEncode(item.Remarks);
}
+ var protoExtra = item.GetProtocolExtra();
var dicQuery = new Dictionary();
- if (!item.GetProtocolExtra().WgPublicKey.IsNullOrEmpty())
+ if (!protoExtra.WgPublicKey.IsNullOrEmpty())
{
- dicQuery.Add("publickey", Utils.UrlEncode(item.GetProtocolExtra().WgPublicKey));
+ dicQuery.Add("publickey", Utils.UrlEncode(protoExtra.WgPublicKey));
}
- if (!item.GetProtocolExtra().WgReserved.IsNullOrEmpty())
+ if (!protoExtra.WgPresharedKey.IsNullOrEmpty())
{
- dicQuery.Add("reserved", Utils.UrlEncode(item.GetProtocolExtra().WgReserved));
+ dicQuery.Add("presharedkey", Utils.UrlEncode(protoExtra.WgPresharedKey));
}
- if (!item.GetProtocolExtra().WgInterfaceAddress.IsNullOrEmpty())
+ if (!protoExtra.WgReserved.IsNullOrEmpty())
{
- dicQuery.Add("address", Utils.UrlEncode(item.GetProtocolExtra().WgInterfaceAddress));
+ dicQuery.Add("reserved", Utils.UrlEncode(protoExtra.WgReserved));
+ }
+ if (!protoExtra.WgInterfaceAddress.IsNullOrEmpty())
+ {
+ dicQuery.Add("address", Utils.UrlEncode(protoExtra.WgInterfaceAddress));
+ }
+ if (protoExtra.WgMtu > 0)
+ {
+ dicQuery.Add("mtu", protoExtra.WgMtu.ToString());
}
- dicQuery.Add("mtu", Utils.UrlEncode(item.GetProtocolExtra().WgMtu > 0 ? item.GetProtocolExtra().WgMtu.ToString() : "1280"));
return ToUri(EConfigType.WireGuard, item.Address, item.Port, item.Password, dicQuery, remark);
}
+
+ public static List? ResolveConfig(string strData)
+ {
+ var interfaceDic = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var peerDicList = new List>();
+ var currentDicRef = interfaceDic;
+ using (var reader = new StringReader(strData))
+ {
+ while (reader.ReadLine() is { } line)
+ {
+ if (line.IsNullOrEmpty())
+ {
+ continue;
+ }
+
+ var trimmedLine = line.Trim();
+
+ if (trimmedLine.Equals("[Interface]", StringComparison.OrdinalIgnoreCase))
+ {
+ currentDicRef = interfaceDic;
+ continue;
+ }
+ if (trimmedLine.Equals("[Peer]", StringComparison.OrdinalIgnoreCase))
+ {
+ var peerDic = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ peerDicList.Add(peerDic);
+ currentDicRef = peerDic;
+ continue;
+ }
+
+ if (trimmedLine.StartsWith('[') || trimmedLine.StartsWith('#') || trimmedLine.StartsWith(';'))
+ {
+ continue;
+ }
+
+ var idx = line.IndexOf('=');
+ if (idx <= 0)
+ {
+ continue;
+ }
+
+ var key = line[..idx].Trim();
+ var value = line[(idx + 1)..].Trim();
+ var commentPos = value.IndexOfAny(['#', ';']);
+ if (commentPos >= 0)
+ {
+ value = value[..commentPos].TrimEnd();
+ }
+
+ currentDicRef[key] = value;
+ }
+ }
+
+ if (!interfaceDic.TryGetValue("PrivateKey", out var privateKey) || privateKey.IsNullOrEmpty())
+ {
+ return null;
+ }
+
+ var wgMtu = interfaceDic.TryGetValue("MTU", out var mtuStr) && int.TryParse(mtuStr, out var mtuVal) ? mtuVal : 0;
+ var wgInterfaceAddress = interfaceDic.TryGetValue("Address", out var interfaceAddress) ? interfaceAddress : string.Empty;
+
+ var index = 0;
+ var resultList = new List();
+
+ foreach (var peerDic in peerDicList)
+ {
+ if (!peerDic.TryGetValue("Endpoint", out var endpoint) || endpoint.IsNullOrEmpty())
+ {
+ continue;
+ }
+
+ if (!TryParseEndpoint(endpoint, out var peerAddress, out var peerPort))
+ {
+ continue;
+ }
+
+ var protoExtra = new ProtocolExtraItem
+ {
+ WgPublicKey = (peerDic.TryGetValue("PublicKey", out var publicKey) ? publicKey : string.Empty).NullIfEmpty(),
+ WgPresharedKey = (peerDic.TryGetValue("PresharedKey", out var presharedKey) ? presharedKey : string.Empty).NullIfEmpty(),
+ WgInterfaceAddress = wgInterfaceAddress,
+ WgReserved = (peerDic.TryGetValue("Reserved", out var reserved) ? reserved : string.Empty).NullIfEmpty(),
+ WgMtu = wgMtu > 0 ? wgMtu : null,
+ };
+
+ var item = new ProfileItem
+ {
+ Remarks = $"{nameof(EConfigType.WireGuard)} Peer {index + 1}",
+ ConfigType = EConfigType.WireGuard,
+ Address = peerAddress,
+ Port = peerPort,
+ Password = privateKey,
+ };
+ item.SetProtocolExtra(protoExtra);
+ resultList.Add(item);
+
+ index += 1;
+ }
+
+ return resultList;
+ }
+
+ private static bool TryParseEndpoint(string endpoint, out string address, out int port)
+ {
+ address = string.Empty;
+ port = 2408;
+
+ var trimmedEndpoint = endpoint.Trim();
+ if (trimmedEndpoint.IsNullOrEmpty())
+ {
+ return false;
+ }
+
+ if (trimmedEndpoint[0] == '[')
+ {
+ var closeIndex = trimmedEndpoint.IndexOf(']');
+ if (closeIndex <= 1)
+ {
+ return false;
+ }
+
+ address = trimmedEndpoint[1..closeIndex].Trim();
+ var portIndex = closeIndex + 1;
+ if (portIndex < trimmedEndpoint.Length && trimmedEndpoint[portIndex] == ':' &&
+ int.TryParse(trimmedEndpoint[(portIndex + 1)..].Trim(), out var bracketedPort) && bracketedPort is > 0 and <= 65535)
+ {
+ port = bracketedPort;
+ }
+
+ return address.IsNotEmpty();
+ }
+
+ var lastColonIndex = trimmedEndpoint.LastIndexOf(':');
+ if (lastColonIndex <= 0)
+ {
+ address = trimmedEndpoint;
+ return true;
+ }
+
+ address = trimmedEndpoint[..lastColonIndex].Trim();
+ var portText = trimmedEndpoint[(lastColonIndex + 1)..].Trim();
+ if (address.IsNullOrEmpty())
+ {
+ return false;
+ }
+
+ if (int.TryParse(portText, out var parsedPortValue) && parsedPortValue is > 0 and <= 65535)
+ {
+ port = parsedPortValue;
+ return true;
+ }
+
+ address = trimmedEndpoint;
+ return true;
+ }
}
diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs
index 784f3c43..a5bcde82 100644
--- a/v2rayN/ServiceLib/Models/ConfigItems.cs
+++ b/v2rayN/ServiceLib/Models/ConfigItems.cs
@@ -17,6 +17,8 @@ public class CoreBasicItem
public string? SendThrough { get; set; }
+ public string? BindInterface { get; set; }
+
public bool EnableFragment { get; set; }
public bool EnableCacheFile4Sbox { get; set; } = true;
@@ -159,6 +161,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/CoreConfigContext.cs b/v2rayN/ServiceLib/Models/CoreConfigContext.cs
index 8123f19f..4b64efca 100644
--- a/v2rayN/ServiceLib/Models/CoreConfigContext.cs
+++ b/v2rayN/ServiceLib/Models/CoreConfigContext.cs
@@ -17,4 +17,7 @@ public record CoreConfigContext
// TUN Compatibility
public bool IsTunEnabled { get; init; } = false;
public HashSet ProtectDomainList { get; init; } = [];
+
+ public bool IsWindows { get; init; }
+ public bool IsMacOS { get; init; }
}
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..0e2bb530 100644
--- a/v2rayN/ServiceLib/Models/SingboxConfig.cs
+++ b/v2rayN/ServiceLib/Models/SingboxConfig.cs
@@ -173,7 +173,7 @@ public class Peer4Sbox
public string? pre_shared_key { get; set; }
public List allowed_ips { get; set; }
public int? persistent_keepalive_interval { get; set; }
- public List reserved { get; set; }
+ public List? reserved { get; set; }
}
public class Tls4Sbox
@@ -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..10420dad 100644
--- a/v2rayN/ServiceLib/Models/V2rayConfig.cs
+++ b/v2rayN/ServiceLib/Models/V2rayConfig.cs
@@ -163,6 +163,7 @@ public class WireguardPeer4Ray
{
public string endpoint { get; set; }
public string publicKey { get; set; }
+ public string? preSharedKey { get; set; }
}
public class VnextItem4Ray
@@ -500,12 +501,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 +538,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 05851f08..43d5cac9 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 {
@@ -1023,6 +1023,15 @@ namespace ServiceLib.Resx {
}
}
+ ///
+ /// 查找类似 Export v2rayN Internal Share Link to Clipboard 的本地化字符串。
+ ///
+ public static string menuExport2InnerUri {
+ get {
+ return ResourceManager.GetString("menuExport2InnerUri", resourceCulture);
+ }
+ }
+
///
/// 查找类似 Export Share Link to Clipboard 的本地化字符串。
///
@@ -1860,6 +1869,15 @@ namespace ServiceLib.Resx {
}
}
+ ///
+ /// 查找类似 Test Configurations UDP Delay 的本地化字符串。
+ ///
+ public static string menuUdpTestServer {
+ get {
+ return ResourceManager.GetString("menuUdpTestServer", resourceCulture);
+ }
+ }
+
///
/// 查找类似 {0} Website 的本地化字符串。
///
@@ -2872,7 +2890,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 +2952,6 @@ namespace ServiceLib.Resx {
}
}
- ///
- /// 查找类似 EchForceQuery 的本地化字符串。
- ///
- public static string TbEchForceQuery {
- get {
- return ResourceManager.GetString("TbEchForceQuery", resourceCulture);
- }
- }
-
///
/// 查找类似 Edit 的本地化字符串。
///
@@ -3222,6 +3231,15 @@ namespace ServiceLib.Resx {
}
}
+ ///
+ /// 查找类似 MTU 的本地化字符串。
+ ///
+ public static string TbMtu {
+ get {
+ return ResourceManager.GetString("TbMtu", resourceCulture);
+ }
+ }
+
///
/// 查找类似 Transport protocol(network) 的本地化字符串。
///
@@ -3312,6 +3330,15 @@ namespace ServiceLib.Resx {
}
}
+ ///
+ /// 查找类似 PreSharedKey 的本地化字符串。
+ ///
+ public static string TbPreSharedKey {
+ get {
+ return ResourceManager.GetString("TbPreSharedKey", resourceCulture);
+ }
+ }
+
///
/// 查找类似 Socks port 的本地化字符串。
///
@@ -3430,7 +3457,7 @@ namespace ServiceLib.Resx {
}
///
- /// 查找类似 Reserved (2,3,4) 的本地化字符串。
+ /// 查找类似 Reserved 的本地化字符串。
///
public static string TbReserved {
get {
@@ -3681,6 +3708,24 @@ namespace ServiceLib.Resx {
}
}
+ ///
+ /// 查找类似 Bind Interface 的本地化字符串。
+ ///
+ public static string TbSettingsBindInterface {
+ get {
+ return ResourceManager.GetString("TbSettingsBindInterface", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems and TUN mode 的本地化字符串。
+ ///
+ public static string TbSettingsBindInterfaceTip {
+ get {
+ return ResourceManager.GetString("TbSettingsBindInterfaceTip", resourceCulture);
+ }
+ }
+
///
/// 查找类似 Users in China region can ignore this item 的本地化字符串。
///
@@ -4176,15 +4221,6 @@ namespace ServiceLib.Resx {
}
}
- ///
- /// 查找类似 Custom DNS (multiple, separated by commas (,)) 的本地化字符串。
- ///
- public static string TbSettingsRemoteDNS {
- get {
- return ResourceManager.GetString("TbSettingsRemoteDNS", resourceCulture);
- }
- }
-
///
/// 查找类似 Route Only 的本地化字符串。
///
@@ -4383,15 +4419,6 @@ namespace ServiceLib.Resx {
}
}
- ///
- /// 查找类似 MTU 的本地化字符串。
- ///
- public static string TbSettingsTunMtu {
- get {
- return ResourceManager.GetString("TbSettingsTunMtu", resourceCulture);
- }
- }
-
///
/// 查找类似 Stack 的本地化字符串。
///
@@ -4419,6 +4446,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 0d8f8077..a74797f3 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
@@ -1065,7 +1062,7 @@
پشته شبکه
-
+
MTU
@@ -1078,7 +1075,7 @@
کلید خصوصی
- Reserved (2,3,4)
+ Reserved
آدرس (IPv4, IPv6)
@@ -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
@@ -1704,9 +1698,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
Legacy TUN Protect
-
- For multi-interface environments, enter the local machine's IPv4 address
-
Camouflage domain
@@ -1722,4 +1713,31 @@ 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
+
+
+ Local outbound address (SendThrough)
+
+
+ For multi-interface environments, enter the local machine's IPv4 address
+
+
+ Please fill in the correct IPv4 address for SendThrough.
+
+
+ Bind Interface
+
+
+ For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems and TUN mode
+
+
+ PreSharedKey
+
+
+ Export v2rayN Internal Share Link to Clipboard
+
\ No newline at end of file
diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx
index 2a9af52d..1c95e601 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
@@ -1062,7 +1059,7 @@
Pile de protocoles
-
+
MTU
@@ -1075,7 +1072,7 @@
PrivateKey
- Reserved (2,3,4)
+ Reserved
Address (IPv4,IPv6)
@@ -1314,15 +1311,6 @@
Le mot de passe sera vérifié en ligne de commande. En cas d’échec ou de dysfonctionnement, redémarrez l’application. Il n’est pas stocké et doit être saisi à chaque redémarrage.
-
- Adresse sortante locale (SendThrough)
-
-
- Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale.
-
-
- Veuillez saisir l’adresse IPv4 correcte de SendThrough.
-
Mode XHTTP
@@ -1599,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
@@ -1725,4 +1710,31 @@ 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.
+
+ Adresse sortante locale (SendThrough)
+
+
+ Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale
+
+
+ Veuillez saisir l’adresse IPv4 correcte de SendThrough.
+
+
+ Lier l'interface
+
+
+ Pour les environnements multi-interfaces, entrez le nom de l'interface à lier. Ne fonctionne que sur les systèmes Windows et en mode TUN
+
+
+ Test Configurations UDP Delay
+
+
+ UDP Test Url
+
+
+ PreSharedKey
+
+
+ Export v2rayN Internal Share Link to Clipboard
+
\ No newline at end of file
diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx
index 866a08a9..4fa32274 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
@@ -1065,7 +1062,7 @@
Hálózati verem
-
+
MTU
@@ -1078,7 +1075,7 @@
Privát kulcs
- Fenntartott (2,3,4)
+ Fenntartott
Cím (IPv4, IPv6)
@@ -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
@@ -1704,9 +1698,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
Legacy TUN Protect
-
- For multi-interface environments, enter the local machine's IPv4 address
-
Álcázási tartomány
@@ -1722,4 +1713,31 @@ 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
+
+
+ Local outbound address (SendThrough)
+
+
+ For multi-interface environments, enter the local machine's IPv4 address
+
+
+ Please fill in the correct IPv4 address for SendThrough.
+
+
+ Bind Interface
+
+
+ For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems and TUN mode
+
+
+ PreSharedKey
+
+
+ Export v2rayN Internal Share Link to Clipboard
+
\ No newline at end of file
diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx
index a1c4ebb7..695cda08 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
@@ -1065,7 +1062,7 @@
Stack
-
+
MTU
@@ -1078,7 +1075,7 @@
Private Key
- Reserved (2,3,4)
+ Reserved
Address (IPv4, IPv6)
@@ -1317,15 +1314,6 @@
The password will be validated via the command line. If a validation error causes the application to malfunction, please restart the application. The password will not be stored and must be entered again after each restart.
-
- Local outbound address (SendThrough)
-
-
- For multi-interface environments, enter the local machine's IPv4 address
-
-
- Please fill in the correct IPv4 address for SendThrough.
-
xhttp mode
@@ -1602,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
@@ -1728,4 +1713,31 @@ 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
+
+
+ Local outbound address (SendThrough)
+
+
+ For multi-interface environments, enter the local machine's IPv4 address
+
+
+ Please fill in the correct IPv4 address for SendThrough.
+
+
+ Bind Interface
+
+
+ For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems and TUN mode
+
+
+ PreSharedKey
+
+
+ Export v2rayN Internal Share Link to Clipboard
+
\ No newline at end of file
diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx
index 7eb2435b..ab380652 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
Шрифт (требуется перезапуск)
@@ -1065,7 +1062,7 @@
Сетевой стек
-
+
MTU
@@ -1078,7 +1075,7 @@
Приватный ключ
- Зарезервировано (2, 3, 4)
+ Зарезервировано
Адрес (IPv4, IPv6)
@@ -1321,7 +1318,7 @@
XHTTP-режим
- Raw JSON, format: { XHTTP Object }
+ Сырой JSON, формат: { XHTTP Object }
Сворачивать в трей при закрытии окна
@@ -1348,7 +1345,7 @@
Включить второй смешанный порт
- socks: локальный порт, socks2: второй локальный порт, socks3: LAN порт
+ socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт
Тема
@@ -1390,7 +1387,7 @@
Mldsa65Verify
- Добавить сервер [Anytls]
+ Добавить сервер [AnyTLS]
Удалённый DNS
@@ -1593,9 +1590,6 @@
EchConfigList
-
- EchForceQuery
-
Полный сертификат (цепочка) в формате PEM
@@ -1704,22 +1698,46 @@
Устаревшая защита TUN (Legacy Protect)
-
- For multi-interface environments, enter the local machine's IPv4 address
-
Камуфляжный домен
- 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).
+
+
+ Тест UDP-задержки конфигураций
+
+
+ URL для UDP-теста
+
+
+ Локальный исходящий адрес (SendThrough)
+
+
+ Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера
+
+
+ Укажите корректный IPv4-адрес для SendThrough.
+
+
+ Привязать интерфейс
+
+
+ Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для привязки. Работает только в Windows и режиме TUN
+
+
+ PreSharedKey
+
+
+ Export v2rayN Internal Share Link to Clipboard
\ No newline at end of file
diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx
index e27df42c..7fead923 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 格式,点击查看文档
普通分组此处请留空
@@ -1062,7 +1059,7 @@
协议栈
-
+
MTU
@@ -1075,7 +1072,7 @@
PrivateKey
- Reserved (2,3,4)
+ Reserved
Address (IPv4,IPv6)
@@ -1314,15 +1311,6 @@
密码将调用命令行校验,如果因为校验错误导致无法正常运行时,请重启本应用。 密码不会存储,每次重启后都需要再次输入。
-
- 本地出站地址 (SendThrough)
-
-
- 用于多网口环境,请填写本机 IPv4 地址
-
-
- 请填写正确的 SendThrough IPv4 地址。
-
XHTTP 模式
@@ -1599,9 +1587,6 @@
EchConfigList
-
- EchForceQuery
-
完整证书(链),PEM 格式
@@ -1725,4 +1710,31 @@
仅用于抓取自签证书,存在中间人风险。
+
+ 测试 UDP 延迟 (多选)
+
+
+ UDP 测试地址
+
+
+ 本地出站地址 (SendThrough)
+
+
+ 用于多网口环境,请填写本机 IPv4 地址
+
+
+ 请填写正确的 SendThrough IPv4 地址。
+
+
+ 绑定网口
+
+
+ 用于多网口环境,填写要绑定的网口名称,仅生效于 Windows 系统和 TUN 模式
+
+
+ PreSharedKey
+
+
+ 导出 v2rayN 内部分享链接至剪贴板 (多选)
+
\ No newline at end of file
diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx
index d06d28dc..dd8b5e30 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 格式,點擊查看說明
普通分組此處請留空
@@ -1062,7 +1059,7 @@
協定堆疊
-
+
MTU
@@ -1075,7 +1072,7 @@
PrivateKey
- Reserved (2,3,4)
+ Reserved
Address (Ipv4,Ipv6)
@@ -1590,9 +1587,6 @@
EchConfigList
-
- EchForceQuery
-
完整憑證(鏈),PEM 格式
@@ -1699,10 +1693,7 @@
ICMP 路由策略
- Legacy TUN Protect
-
-
- For multi-interface environments, enter the local machine's IPv4 address
+ 舊版 TUN 保護
偽裝域名
@@ -1719,4 +1710,31 @@
僅用於抓取自簽證書,存在中間人風險。
-
\ No newline at end of file
+
+ 測試 UDP 延遲(多選)
+
+
+ UDP 測試網址
+
+
+ 本機出站位址 (SendThrough)
+
+
+ 適用於多網路介面環境,請填寫本機 IPv4 位址
+
+
+ 請填寫正確的 SendThrough IPv4 位址。
+
+
+ 綁定網路介面
+
+
+ 適用於多網路介面環境,請填寫要綁定的介面名稱;Windows 系統有效,其他系統僅在 TUN 模式下生效。
+
+
+ PreSharedKey
+
+
+ 匯出 v2rayN 內部分享連結至剪貼簿(多選)
+
+
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/CoreConfigSingboxService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs
index fe00861d..9cddbfe8 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/CoreConfigSingboxService.cs
@@ -57,6 +57,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
ConvertGeo2Ruleset();
+ ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
@@ -170,6 +171,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
_coreConfig.route.rules.Add(rule);
}
+ ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig);
@@ -229,6 +231,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
listen_port = port,
type = EInboundProtocol.mixed.ToString(),
});
+ ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs
index d20aec1c..4fffe27c 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs
@@ -59,22 +59,38 @@ public partial class CoreConfigSingboxService
return JsonUtils.Serialize(fullConfigTemplateNode);
}
- private void ApplyOutboundSendThrough()
+ private void ApplyOutboundBindInterface()
{
- var sendThrough = _config.CoreBasicItem.SendThrough?.TrimEx();
+ var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
+ if (bindInterface.IsNullOrEmpty())
+ {
+ return;
+ }
+ if (!(context.IsTunEnabled || context.IsWindows))
+ {
+ return;
+ }
foreach (var outbound in _coreConfig.outbounds ?? [])
{
- outbound.inet4_bind_address = ShouldApplySendThrough(outbound, sendThrough) ? sendThrough : null;
+ outbound.bind_interface = ShouldBindNet(outbound) ? bindInterface : null;
}
}
- private static bool ShouldApplySendThrough(Outbound4Sbox outbound, string? sendThrough)
+ private void ApplyOutboundSendThrough()
{
+ var sendThrough = _config.CoreBasicItem.SendThrough?.TrimEx();
if (sendThrough.IsNullOrEmpty())
{
- return false;
+ return;
}
+ foreach (var outbound in _coreConfig.outbounds ?? [])
+ {
+ outbound.inet4_bind_address = ShouldBindNet(outbound) ? sendThrough : null;
+ }
+ }
+ private static bool ShouldBindNet(Outbound4Sbox outbound)
+ {
if (outbound.type is "direct" or "block" or "dns" or "selector" or "urltest")
{
return false;
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/SingboxInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs
index 3d05b987..9f936989 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxInboundService.cs
@@ -8,10 +8,10 @@ public partial class CoreConfigSingboxService
{
var listen = "0.0.0.0";
var listenPort = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
+ var isUsingLocalMixedPort = _node.Address == Global.Loopback && _node.Port == listenPort;
_coreConfig.inbounds = [];
- if (!context.IsTunEnabled
- || (context.IsTunEnabled && _node.Address != Global.Loopback && _node.Port != listenPort))
+ if (!context.IsTunEnabled || !isUsingLocalMixedPort)
{
var inbound = new Inbound4Sbox()
{
@@ -77,7 +77,7 @@ public partial class CoreConfigSingboxService
}
var tunInbound = JsonUtils.Deserialize(EmbedUtils.GetEmbedText(Global.TunSingboxInboundFileName)) ?? new Inbound4Sbox { };
- tunInbound.interface_name = Utils.IsMacOS() ? $"utun{new Random().Next(99)}" : "singbox_tun";
+ tunInbound.interface_name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "singbox_tun";
tunInbound.mtu = _config.TunModeItem.Mtu;
tunInbound.auto_route = _config.TunModeItem.AutoRoute;
tunInbound.strict_route = _config.TunModeItem.StrictRoute;
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs
index 5b35db01..8d36b353 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;
@@ -309,7 +309,7 @@ public partial class CoreConfigSingboxService
{
var protocolExtra = _node.GetProtocolExtra();
- endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress);
+ endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"];
endpoint.type = Global.ProtocolTypes[_node.ConfigType];
switch (_node.ConfigType)
@@ -318,13 +318,12 @@ public partial class CoreConfigSingboxService
{
var peer = new Peer4Sbox
{
- public_key = protocolExtra.WgPublicKey,
+ public_key = protocolExtra.WgPublicKey ?? string.Empty,
pre_shared_key = protocolExtra.WgPresharedKey,
- reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(int.Parse).ToList(),
+ reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(s => s.Trim()).Select(int.Parse).ToList(),
address = _node.Address,
port = _node.Port,
- // TODO default ["0.0.0.0/0", "::/0"]
- allowed_ips = new() { "0.0.0.0/0", "::/0" },
+ allowed_ips = ["0.0.0.0/0", "::/0"],
};
endpoint.private_key = _node.Password;
endpoint.mtu = protocolExtra.WgMtu > 0 ? protocolExtra.WgMtu : Global.TunMtus.First();
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..85963bb4 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs
@@ -60,6 +60,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{
ApplyOutboundFragment();
}
+ ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
var finalRule = BuildFinalRule();
@@ -161,6 +162,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);
@@ -198,6 +204,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{
ApplyOutboundFragment();
}
+ ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
ret.Success = true;
@@ -256,6 +263,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());
@@ -264,6 +276,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{
ApplyOutboundFragment();
}
+ ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs
index e6bc48cb..a218ac13 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs
@@ -134,22 +134,59 @@ public partial class CoreConfigV2rayService
return JsonUtils.Serialize(fullConfigTemplateNode);
}
- private void ApplyOutboundSendThrough()
+ private void ApplyOutboundBindInterface()
{
- var sendThrough = _config.CoreBasicItem.SendThrough?.TrimEx();
+ var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
+ if (bindInterface.IsNullOrEmpty())
+ {
+ return;
+ }
+ if (!(context.IsTunEnabled || context.IsWindows))
+ {
+ return;
+ }
foreach (var outbound in _coreConfig.outbounds ?? [])
{
- outbound.sendThrough = ShouldApplySendThrough(outbound, sendThrough) ? sendThrough : null;
+ if (!ShouldBindNet(outbound))
+ {
+ continue;
+ }
+ outbound.streamSettings ??= new();
+ outbound.streamSettings.sockopt ??= new();
+ outbound.streamSettings.sockopt.Interface = bindInterface;
+ // xhttp download bind interface
+ if (outbound?.streamSettings?.xhttpSettings?.extra is null)
+ {
+ continue;
+ }
+ var xhttpExtra = JsonUtils.ParseJson(JsonUtils.Serialize(outbound.streamSettings.xhttpSettings!.extra));
+ if (xhttpExtra is not JsonObject xhttpExtraObject
+ || xhttpExtraObject["downloadSettings"] is not JsonObject downloadSettings)
+ {
+ continue;
+ }
+ var sockopt = downloadSettings["sockopt"] as JsonObject ?? new JsonObject();
+ sockopt["interface"] = bindInterface;
+ downloadSettings["sockopt"] = sockopt;
+ outbound.streamSettings.xhttpSettings.extra = xhttpExtraObject;
}
}
- private static bool ShouldApplySendThrough(Outbounds4Ray outbound, string? sendThrough)
+ private void ApplyOutboundSendThrough()
{
+ var sendThrough = _config.CoreBasicItem.SendThrough?.TrimEx();
if (sendThrough.IsNullOrEmpty())
{
- return false;
+ return;
}
+ foreach (var outbound in _coreConfig.outbounds ?? [])
+ {
+ outbound.sendThrough = ShouldBindNet(outbound) ? sendThrough : null;
+ }
+ }
+ private static bool ShouldBindNet(Outbounds4Ray outbound)
+ {
if (outbound.protocol is "freedom" or "blackhole" or "dns" or "loopback")
{
return false;
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs
index 7faae5b1..abb3e134 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/V2rayInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs
index 985f597f..c4555ed9 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs
@@ -54,12 +54,17 @@ public partial class CoreConfigV2rayService
_config.TunModeItem.Mtu = Global.TunMtus.First();
}
var tunInbound = JsonUtils.Deserialize(EmbedUtils.GetEmbedText(Global.V2raySampleTunInbound)) ?? new Inbounds4Ray { };
- tunInbound.settings.name = Utils.IsMacOS() ? $"utun{new Random().Next(99)}" : "xray_tun";
+ tunInbound.settings.name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "xray_tun";
tunInbound.settings.MTU = _config.TunModeItem.Mtu;
if (_config.TunModeItem.EnableIPv6Address == false)
{
tunInbound.settings.gateway = ["172.18.0.1/30"];
}
+ var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
+ if (!bindInterface.IsNullOrEmpty())
+ {
+ tunInbound.settings.autoOutboundsInterface = bindInterface;
+ }
tunInbound.sniffing = inbound.sniffing;
_coreConfig.inbounds.Add(tunInbound);
}
diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs
index b058dfa1..f3836960 100644
--- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs
+++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs
@@ -258,13 +258,14 @@ public partial class CoreConfigV2rayService
var peer = new WireguardPeer4Ray
{
publicKey = protocolExtra.WgPublicKey ?? "",
- endpoint = address + ":" + _node.Port.ToString()
+ endpoint = address + ":" + _node.Port.ToString(),
+ preSharedKey = protocolExtra.WgPresharedKey,
};
var setting = new Outboundsettings4Ray
{
- address = Utils.String2List(protocolExtra.WgInterfaceAddress),
+ address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"],
secretKey = _node.Password,
- reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(int.Parse).ToList(),
+ reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(s => s.Trim()).Select(int.Parse).ToList(),
mtu = protocolExtra.WgMtu > 0 ? protocolExtra.WgMtu : Global.TunMtus.First(),
peers = [peer]
};
@@ -328,6 +329,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 +343,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 +384,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 +393,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 +448,7 @@ public partial class CoreConfigV2rayService
case nameof(ETransport.kcp):
KcpSettings4Ray kcpSettings = new()
{
- mtu = _config.KcpItem.Mtu,
+ mtu = kcpMtu,
tti = _config.KcpItem.Tti
};
@@ -546,6 +553,7 @@ public partial class CoreConfigV2rayService
FillOutboundMux(outbound);
break;
+
case nameof(ETransport.grpc):
GrpcSettings4Ray grpcSettings = new()
{
@@ -569,7 +577,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 b54ce0ea..9012e26f 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);
@@ -332,6 +419,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..169e8a44 100644
--- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs
+++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs
@@ -53,8 +53,9 @@ public class AddServerViewModel : MyReactiveObject
[Reactive]
public string WgPublicKey { get; set; }
- //[Reactive]
- //public string WgPresharedKey { get; set; }
+ [Reactive]
+ public string WgPresharedKey { get; set; }
+
[Reactive]
public string WgInterfaceAddress { get; set; }
@@ -106,6 +107,9 @@ public class AddServerViewModel : MyReactiveObject
[Reactive]
public string KcpSeed { get; set; }
+ [Reactive]
+ public int? KcpMtu { get; set; }
+
public string TransportHeaderType
{
get => SelectedSource.GetNetwork() switch
@@ -156,17 +160,8 @@ public class AddServerViewModel : MyReactiveObject
switch (SelectedSource.GetNetwork())
{
case nameof(ETransport.raw):
- Host = value;
- break;
-
case nameof(ETransport.ws):
- Host = value;
- break;
-
case nameof(ETransport.httpupgrade):
- Host = value;
- break;
-
case nameof(ETransport.xhttp):
Host = value;
break;
@@ -199,13 +194,7 @@ public class AddServerViewModel : MyReactiveObject
break;
case nameof(ETransport.ws):
- Path = value;
- break;
-
case nameof(ETransport.httpupgrade):
- Path = value;
- break;
-
case nameof(ETransport.xhttp):
Path = value;
break;
@@ -287,26 +276,27 @@ 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;
+ WgPresharedKey = protocolExtra.WgPresharedKey ?? 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 +308,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 +372,7 @@ public class AddServerViewModel : MyReactiveObject
GrpcMode = GrpcMode.NullIfEmpty(),
KcpHeaderType = KcpHeaderType.NullIfEmpty(),
KcpSeed = KcpSeed.NullIfEmpty(),
+ KcpMtu = KcpMtu > 0 ? KcpMtu : null,
};
SelectedSource.SetProtocolExtra(SelectedSource.GetProtocolExtra() with
@@ -396,6 +388,7 @@ public class AddServerViewModel : MyReactiveObject
VlessEncryption = VlessEncryption.NullIfEmpty(),
SsMethod = SsMethod.NullIfEmpty(),
WgPublicKey = WgPublicKey.NullIfEmpty(),
+ WgPresharedKey = WgPresharedKey.NullIfEmpty(),
WgInterfaceAddress = WgInterfaceAddress.NullIfEmpty(),
WgReserved = WgReserved.NullIfEmpty(),
WgMtu = WgMtu >= 576 ? WgMtu : null,
diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs
index c1d7588f..2fc1b40d 100644
--- a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs
+++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs
@@ -24,6 +24,7 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public string defFingerprint { get; set; }
[Reactive] public string defUserAgent { get; set; }
[Reactive] public string sendThrough { get; set; }
+ [Reactive] public string bindInterface { get; set; }
[Reactive] public string mux4SboxProtocol { get; set; }
[Reactive] public bool enableCacheFile4Sbox { get; set; }
[Reactive] public int? hyUpMbps { get; set; }
@@ -62,6 +63,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; }
@@ -161,7 +163,8 @@ public class OptionSettingViewModel : MyReactiveObject
defAllowInsecure = _config.CoreBasicItem.DefAllowInsecure;
defFingerprint = _config.CoreBasicItem.DefFingerprint;
defUserAgent = _config.CoreBasicItem.DefUserAgent;
- sendThrough = _config.CoreBasicItem.SendThrough;
+ sendThrough = _config.CoreBasicItem.SendThrough ?? string.Empty;
+ bindInterface = _config.CoreBasicItem.BindInterface ?? string.Empty;
mux4SboxProtocol = _config.Mux4SboxItem.Protocol;
enableCacheFile4Sbox = _config.CoreBasicItem.EnableCacheFile4Sbox;
hyUpMbps = _config.HysteriaItem.UpMbps;
@@ -201,6 +204,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;
@@ -305,7 +309,7 @@ public class OptionSettingViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort);
return;
}
- var sendThroughValue = sendThrough?.TrimEx();
+ var sendThroughValue = sendThrough.TrimEx();
if (sendThroughValue.IsNotEmpty() && !Utils.IsIpv4(sendThroughValue))
{
NoticeManager.Instance.Enqueue(ResUI.FillCorrectSendThroughIPv4);
@@ -353,7 +357,8 @@ public class OptionSettingViewModel : MyReactiveObject
_config.CoreBasicItem.DefAllowInsecure = defAllowInsecure;
_config.CoreBasicItem.DefFingerprint = defFingerprint;
_config.CoreBasicItem.DefUserAgent = defUserAgent;
- _config.CoreBasicItem.SendThrough = sendThrough?.TrimEx();
+ _config.CoreBasicItem.SendThrough = sendThrough.TrimEx();
+ _config.CoreBasicItem.BindInterface = bindInterface.TrimEx();
_config.Mux4SboxItem.Protocol = mux4SboxProtocol;
_config.CoreBasicItem.EnableCacheFile4Sbox = enableCacheFile4Sbox;
_config.HysteriaItem.UpMbps = hyUpMbps ?? 0;
@@ -377,6 +382,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..609ae0d1 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; }
@@ -71,6 +72,7 @@ public class ProfilesViewModel : MyReactiveObject
public ReactiveCommand Export2ClientConfigClipboardCmd { get; }
public ReactiveCommand Export2ShareUrlCmd { get; }
public ReactiveCommand Export2ShareUrlBase64Cmd { get; }
+ public ReactiveCommand Export2InnerUriCmd { get; }
public ReactiveCommand AddSubCmd { get; }
public ReactiveCommand EditSubCmd { get; }
@@ -178,6 +180,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);
@@ -207,6 +213,10 @@ public class ProfilesViewModel : MyReactiveObject
{
await Export2ShareUrlAsync(true);
}, canEditRemove);
+ Export2InnerUriCmd = ReactiveCommand.CreateFromTask(async () =>
+ {
+ await Export2InnerUrlAsync();
+ }, canEditRemove);
//Subscription
AddSubCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -835,6 +845,32 @@ public class ProfilesViewModel : MyReactiveObject
}
}
+ public async Task Export2InnerUrlAsync()
+ {
+ var lstSelected = await GetProfileItems(true);
+ if (lstSelected == null)
+ {
+ return;
+ }
+
+ var result = string.Empty;
+
+ await Task.Run(() =>
+ {
+ result = InnerFmt.ToUri(lstSelected);
+ });
+
+ if (!result.IsNullOrEmpty())
+ {
+ await _updateView?.Invoke(EViewAction.SetClipboardData, result);
+ NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
+ }
+ else
+ {
+ NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
+ }
+ }
+
#endregion Add Servers
#region Subscription
diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml
index f29afb7f..dbf031b1 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" />
@@ -508,7 +508,7 @@
Grid.Row="2"
ColumnDefinitions="300,Auto"
IsVisible="False"
- RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
+ RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
+ Text="{x:Static resx:ResUI.TbPreSharedKey}" />
+ HorizontalAlignment="Left" />
+
+
+
+ Text="{x:Static resx:ResUI.TbMtu}" />
+ Watermark="1280" />
+ 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;
@@ -192,6 +185,7 @@ public partial class AddServerWindow : WindowBase
case EConfigType.WireGuard:
this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId9.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.WgPublicKey, v => v.txtPublicKey9.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.WgPresharedKey, v => v.txtPreSharedKey9.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.WgReserved, v => v.txtPath9.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.WgInterfaceAddress, v => v.txtRequestHost9.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.WgMtu, v => v.txtShortId9.Text).DisposeWith(disposables);
@@ -218,6 +212,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 +240,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 +331,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 6ccce9e5..2d063205 100644
--- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml
+++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml
@@ -37,8 +37,8 @@
+ ColumnDefinitions="Auto,Auto,*"
+ RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
+
+
+
+
+ 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" />-->
+
+
+
-
@@ -877,7 +910,7 @@
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
- Text="{x:Static resx:ResUI.TbSettingsTunMtu}" />
+ Text="{x:Static resx:ResUI.TbMtu}" />
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;
@@ -81,6 +82,7 @@ public partial class OptionSettingWindow : WindowBase
this.Bind(ViewModel, vm => vm.defAllowInsecure, v => v.togdefAllowInsecure.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defFingerprint, v => v.cmbdefFingerprint.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defUserAgent, v => v.cmbdefUserAgent.SelectedValue).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.bindInterface, v => v.txtbindInterface.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.sendThrough, v => v.txtsendThrough.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.mux4SboxProtocol, v => v.cmbmux4SboxProtocol.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.enableCacheFile4Sbox, v => v.togenableCacheFile4Sbox.IsChecked).DisposeWith(disposables);
@@ -92,7 +94,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);
@@ -102,6 +104,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..a567ae6a 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 6595409f..69e57684 100644
--- a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
+++ b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml
@@ -65,6 +65,7 @@
+
@@ -444,21 +445,44 @@
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
- Text="{x:Static resx:ResUI.TbSettingsSendThrough}" />
+ Text="{x:Static resx:ResUI.TbSettingsBindInterface}" />
+ Style="{StaticResource DefTextBox}" />
+
+
+
+
@@ -491,7 +515,7 @@
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
- Text="{x:Static resx:ResUI.TbSettingsTunMtu}" />
+ Text="{x:Static resx:ResUI.TbMtu}" />
+
+
@@ -880,10 +906,26 @@
Margin="{StaticResource Margin8}"
VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}"
+ Text="{x:Static resx:ResUI.TbSettingsUdpTestUrl}" />
+
+
+
+ Text="{x:Static resx:ResUI.TbMtu}" />
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;
@@ -79,6 +80,7 @@ public partial class OptionSettingWindow
this.Bind(ViewModel, vm => vm.defFingerprint, v => v.cmbdefFingerprint.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.defUserAgent, v => v.cmbdefUserAgent.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.sendThrough, v => v.txtsendThrough.Text).DisposeWith(disposables);
+ this.Bind(ViewModel, vm => vm.bindInterface, v => v.txtbindInterface.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.mux4SboxProtocol, v => v.cmbmux4SboxProtocol.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.enableCacheFile4Sbox, v => v.togenableCacheFile4Sbox.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.hyUpMbps, v => v.txtUpMbps.Text).DisposeWith(disposables);
@@ -107,6 +109,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..5a25e4c7 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" />
+
+