mirror of
https://github.com/2dust/v2rayN.git
synced 2026-05-18 07:34:36 +03:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7973840ce | |||
| 212071681d | |||
| 0f9bfeb275 | |||
| f5059f1165 | |||
| 2dc967bc04 | |||
| 75ea81dd69 | |||
| d13f7a4db6 | |||
| f073b14fcc | |||
| 1a44af33d0 | |||
| 8450f2e420 | |||
| 37ef25cbfe | |||
| 0fac18ba95 | |||
| 3ccd59d1dc | |||
| 6c38a08f12 | |||
| f8f7fee461 | |||
| 3e157b0d62 | |||
| 49d197e37f | |||
| b6f2912f29 | |||
| 05e349e45c | |||
| ae662a628d | |||
| 6e85f79852 | |||
| 0af29f50c0 | |||
| ee6ae3d91d |
@@ -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
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>7.21.0</Version>
|
||||
<Version>7.21.2</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -9,105 +9,105 @@ namespace ServiceLib.Tests.CoreConfig.Context;
|
||||
|
||||
public class CoreConfigContextBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ResolveNodeAsync_DirectCycleDependency_ShouldFailWithCycleError()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
[Fact]
|
||||
public async Task ResolveNodeAsync_DirectCycleDependency_ShouldFailWithCycleError()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var groupAId = NewId("group-a");
|
||||
var groupBId = NewId("group-b");
|
||||
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId]);
|
||||
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupAId]);
|
||||
var groupAId = NewId("group-a");
|
||||
var groupBId = NewId("group-b");
|
||||
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId]);
|
||||
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupAId]);
|
||||
|
||||
await UpsertProfilesAsync(groupA, groupB);
|
||||
await UpsertProfilesAsync(groupA, groupB);
|
||||
|
||||
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
|
||||
context.AllProxiesMap.Clear();
|
||||
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
|
||||
context.AllProxiesMap.Clear();
|
||||
|
||||
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
|
||||
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
|
||||
|
||||
validatorResult.Success.Should().BeFalse();
|
||||
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
|
||||
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
|
||||
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||
}
|
||||
validatorResult.Success.Should().BeFalse();
|
||||
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
|
||||
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
|
||||
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveNodeAsync_IndirectCycleDependency_ShouldFailWithCycleError()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
[Fact]
|
||||
public async Task ResolveNodeAsync_IndirectCycleDependency_ShouldFailWithCycleError()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var groupAId = NewId("group-a");
|
||||
var groupBId = NewId("group-b");
|
||||
var groupCId = NewId("group-c");
|
||||
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId]);
|
||||
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupCId]);
|
||||
var groupC = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupCId, "group-c", [groupAId]);
|
||||
var groupAId = NewId("group-a");
|
||||
var groupBId = NewId("group-b");
|
||||
var groupCId = NewId("group-c");
|
||||
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId]);
|
||||
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupCId]);
|
||||
var groupC = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupCId, "group-c", [groupAId]);
|
||||
|
||||
await UpsertProfilesAsync(groupA, groupB, groupC);
|
||||
await UpsertProfilesAsync(groupA, groupB, groupC);
|
||||
|
||||
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
|
||||
context.AllProxiesMap.Clear();
|
||||
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
|
||||
context.AllProxiesMap.Clear();
|
||||
|
||||
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
|
||||
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
|
||||
|
||||
validatorResult.Success.Should().BeFalse();
|
||||
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
|
||||
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
|
||||
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||
context.AllProxiesMap.Should().NotContainKey(groupC.IndexId);
|
||||
}
|
||||
validatorResult.Success.Should().BeFalse();
|
||||
validatorResult.Errors.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
|
||||
context.AllProxiesMap.Should().NotContainKey(groupA.IndexId);
|
||||
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||
context.AllProxiesMap.Should().NotContainKey(groupC.IndexId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveNodeAsync_CycleWithValidBranch_ShouldSkipCycleAndKeepValidChild()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
[Fact]
|
||||
public async Task ResolveNodeAsync_CycleWithValidBranch_ShouldSkipCycleAndKeepValidChild()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var groupAId = NewId("group-a");
|
||||
var groupBId = NewId("group-b");
|
||||
var leafId = NewId("leaf");
|
||||
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId, leafId]);
|
||||
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupAId]);
|
||||
var leaf = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, leafId, "leaf");
|
||||
var groupAId = NewId("group-a");
|
||||
var groupBId = NewId("group-b");
|
||||
var leafId = NewId("leaf");
|
||||
var groupA = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupAId, "group-a", [groupBId, leafId]);
|
||||
var groupB = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, groupBId, "group-b", [groupAId]);
|
||||
var leaf = CoreConfigTestFactory.CreateSocksNode(ECoreType.Xray, leafId, "leaf");
|
||||
|
||||
await UpsertProfilesAsync(groupA, groupB, leaf);
|
||||
await UpsertProfilesAsync(groupA, groupB, leaf);
|
||||
|
||||
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
|
||||
context.AllProxiesMap.Clear();
|
||||
var context = CoreConfigTestFactory.CreateContext(config, groupA, ECoreType.Xray);
|
||||
context.AllProxiesMap.Clear();
|
||||
|
||||
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
|
||||
var (_, validatorResult) = await CoreConfigContextBuilder.ResolveNodeAsync(context, groupA, false);
|
||||
|
||||
validatorResult.Success.Should().BeTrue();
|
||||
validatorResult.Errors.Should().BeEmpty();
|
||||
validatorResult.Warnings.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
|
||||
validatorResult.Success.Should().BeTrue();
|
||||
validatorResult.Errors.Should().BeEmpty();
|
||||
validatorResult.Warnings.Should().Contain(msg => ContainsCycleDependencyMessage(msg));
|
||||
|
||||
context.AllProxiesMap.Should().ContainKey(leaf.IndexId);
|
||||
context.AllProxiesMap.Should().ContainKey(groupA.IndexId);
|
||||
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||
groupA.GetProtocolExtra().ChildItems.Should().Be(leaf.IndexId);
|
||||
}
|
||||
context.AllProxiesMap.Should().ContainKey(leaf.IndexId);
|
||||
context.AllProxiesMap.Should().ContainKey(groupA.IndexId);
|
||||
context.AllProxiesMap.Should().NotContainKey(groupB.IndexId);
|
||||
groupA.GetProtocolExtra().ChildItems.Should().Be(leaf.IndexId);
|
||||
}
|
||||
|
||||
private static string NewId(string prefix)
|
||||
{
|
||||
return $"{prefix}-{Guid.NewGuid():N}";
|
||||
}
|
||||
private static string NewId(string prefix)
|
||||
{
|
||||
return $"{prefix}-{Guid.NewGuid():N}";
|
||||
}
|
||||
|
||||
private static bool ContainsCycleDependencyMessage(string message)
|
||||
{
|
||||
return message.Contains("cycle dependency", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("循环依赖", StringComparison.Ordinal)
|
||||
|| message.Contains("循環依賴", StringComparison.Ordinal);
|
||||
}
|
||||
private static bool ContainsCycleDependencyMessage(string message)
|
||||
{
|
||||
return message.Contains("cycle dependency", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("循环依赖", StringComparison.Ordinal)
|
||||
|| message.Contains("循環依賴", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static async Task UpsertProfilesAsync(params ProfileItem[] profiles)
|
||||
{
|
||||
SQLiteHelper.Instance.CreateTable<ProfileItem>();
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
await SQLiteHelper.Instance.ReplaceAsync(profile);
|
||||
}
|
||||
}
|
||||
private static async Task UpsertProfilesAsync(params ProfileItem[] profiles)
|
||||
{
|
||||
SQLiteHelper.Instance.CreateTable<ProfileItem>();
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
await SQLiteHelper.Instance.ReplaceAsync(profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<SingboxConfig>(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<SingboxConfig>(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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Send UDP data to a remote endpoint (IP address)
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send UDP data to a remote endpoint (domain name or IP address)
|
||||
/// </summary>
|
||||
/// <param name="host">Domain name or IP address</param>
|
||||
/// <param name="port">Port number</param>
|
||||
/// <param name="data">Data to send</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receive UDP data from remote endpoint
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token to cancel the receive operation</param>
|
||||
/// <returns>Remote endpoint information and received data</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a remote endpoint that can be either an IP address or a domain name
|
||||
/// </summary>
|
||||
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<bool> 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<Socks5AddressData?> 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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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<string> 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<string, Func<IUdpTest>> UdpTestFactories =
|
||||
new Dictionary<string, Func<IUdpTest>>(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<TimeSpan> 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -4,6 +4,7 @@ public enum ESpeedActionType
|
||||
{
|
||||
Tcping,
|
||||
Realping,
|
||||
UdpTest,
|
||||
Speedtest,
|
||||
Mixedtest,
|
||||
FastRealping
|
||||
|
||||
@@ -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<string> 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<string> OutboundTags =
|
||||
[
|
||||
ProxyTag,
|
||||
@@ -673,14 +694,6 @@ public class Global
|
||||
""
|
||||
];
|
||||
|
||||
public static readonly List<string> EchForceQuerys =
|
||||
[
|
||||
"none",
|
||||
"half",
|
||||
"full",
|
||||
""
|
||||
];
|
||||
|
||||
public static readonly List<string> TunIcmpRoutingPolicies =
|
||||
[
|
||||
"rule",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<int> 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<int> 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<int> 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<ProfileItem> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
namespace ServiceLib.Handler.Fmt;
|
||||
|
||||
public class InnerFmt
|
||||
{
|
||||
private static readonly Lazy<string> SessionSalt = new(() => Utils.GetGuid(false));
|
||||
|
||||
public static List<ProfileItem>? Resolve(string strData, string subid)
|
||||
{
|
||||
var list = new List<ProfileItem>();
|
||||
// Overwrite externally imported indexIds to avoid possible sources of attacks
|
||||
var indexIdMap = new Dictionary<string, string>();
|
||||
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<ProfileItem>();
|
||||
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<ProfileItem> 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<ProfileItem>(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<string>(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<string>(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<string>();
|
||||
|
||||
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<string>(out var str) => string.IsNullOrEmpty(str),
|
||||
JsonObject obj => obj.Count == 0,
|
||||
JsonArray arr => arr.Count == 0,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<string, string>();
|
||||
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<ProfileItem>? ResolveConfig(string strData)
|
||||
{
|
||||
var interfaceDic = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var peerDicList = new List<Dictionary<string, string>>();
|
||||
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<string, string>(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<ProfileItem>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -156,6 +158,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]
|
||||
|
||||
@@ -17,4 +17,7 @@ public record CoreConfigContext
|
||||
// TUN Compatibility
|
||||
public bool IsTunEnabled { get; init; } = false;
|
||||
public HashSet<string> ProtectDomainList { get; init; } = [];
|
||||
|
||||
public bool IsWindows { get; init; }
|
||||
public bool IsMacOS { get; init; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -173,7 +173,7 @@ public class Peer4Sbox
|
||||
public string? pre_shared_key { get; set; }
|
||||
public List<string> allowed_ips { get; set; }
|
||||
public int? persistent_keepalive_interval { get; set; }
|
||||
public List<int> reserved { get; set; }
|
||||
public List<int>? 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; }
|
||||
}
|
||||
|
||||
@@ -15,4 +15,5 @@ public record TransportExtraItem
|
||||
|
||||
public string? KcpHeaderType { get; init; }
|
||||
public string? KcpSeed { get; init; }
|
||||
public int? KcpMtu { get; init; }
|
||||
}
|
||||
|
||||
@@ -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<NoiseMask4Ray>? 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; }
|
||||
}
|
||||
|
||||
Generated
+66
-30
@@ -106,7 +106,7 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Please check the Configuration settings first. 的本地化字符串。
|
||||
/// 查找类似 Invalid configuration, please check or reselect 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string CheckServerSettings {
|
||||
get {
|
||||
@@ -1023,6 +1023,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Export v2rayN Internal Share Link to Clipboard 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuExport2InnerUri {
|
||||
get {
|
||||
return ResourceManager.GetString("menuExport2InnerUri", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Export Share Link to Clipboard 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -1860,6 +1869,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Test Configurations UDP Delay 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuUdpTestServer {
|
||||
get {
|
||||
return ResourceManager.GetString("menuUdpTestServer", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 {0} Website 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -2872,7 +2890,7 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Supports DNS Object; Click to view documentation 的本地化字符串。
|
||||
/// 查找类似 Please fill in DNS Object; Click to view documentation 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbDnsObjectDoc {
|
||||
get {
|
||||
@@ -2934,15 +2952,6 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 EchForceQuery 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbEchForceQuery {
|
||||
get {
|
||||
return ResourceManager.GetString("TbEchForceQuery", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Edit 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3222,6 +3231,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 MTU 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbMtu {
|
||||
get {
|
||||
return ResourceManager.GetString("TbMtu", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Transport protocol(network) 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3312,6 +3330,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 PreSharedKey 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbPreSharedKey {
|
||||
get {
|
||||
return ResourceManager.GetString("TbPreSharedKey", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Socks port 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -3430,7 +3457,7 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Reserved (2,3,4) 的本地化字符串。
|
||||
/// 查找类似 Reserved 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbReserved {
|
||||
get {
|
||||
@@ -3681,6 +3708,24 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Bind Interface 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsBindInterface {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsBindInterface", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems and TUN mode 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsBindInterfaceTip {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsBindInterfaceTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Users in China region can ignore this item 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -4149,15 +4194,6 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Custom DNS (multiple, separated by commas (,)) 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsRemoteDNS {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsRemoteDNS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Route Only 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -4356,15 +4392,6 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 MTU 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsTunMtu {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsTunMtu", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Stack 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -4392,6 +4419,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 UDP Test Url 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbSettingsUdpTestUrl {
|
||||
get {
|
||||
return ResourceManager.GetString("TbSettingsUdpTestUrl", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Auth user 的本地化字符串。
|
||||
/// </summary>
|
||||
|
||||
@@ -720,9 +720,6 @@
|
||||
<data name="TbSettingsPass" xml:space="preserve">
|
||||
<value>مجوز احراز هویت</value>
|
||||
</data>
|
||||
<data name="TbSettingsRemoteDNS" xml:space="preserve">
|
||||
<value>سفارشی DNS (multiple, separated by commas (,))</value>
|
||||
</data>
|
||||
<data name="TbSettingsSetUWP" xml:space="preserve">
|
||||
<value>تنظیم کردن Win10 UWP Loopback</value>
|
||||
</data>
|
||||
@@ -1065,7 +1062,7 @@
|
||||
<data name="TbSettingsTunStack" xml:space="preserve">
|
||||
<value>پشته شبکه</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunMtu" xml:space="preserve">
|
||||
<data name="TbMtu" xml:space="preserve">
|
||||
<value>MTU</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
|
||||
@@ -1078,7 +1075,7 @@
|
||||
<value>کلید خصوصی</value>
|
||||
</data>
|
||||
<data name="TbReserved" xml:space="preserve">
|
||||
<value>Reserved (2,3,4)</value>
|
||||
<value>Reserved</value>
|
||||
</data>
|
||||
<data name="TbLocalAddress" xml:space="preserve">
|
||||
<value>آدرس (IPv4, IPv6)</value>
|
||||
@@ -1584,9 +1581,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbEchConfigList" xml:space="preserve">
|
||||
<value>EchConfigList</value>
|
||||
</data>
|
||||
<data name="TbEchForceQuery" xml:space="preserve">
|
||||
<value>EchForceQuery</value>
|
||||
</data>
|
||||
<data name="TbFullCertTips" xml:space="preserve">
|
||||
<value>Full certificate (chain), PEM format</value>
|
||||
</data>
|
||||
@@ -1695,9 +1689,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbLegacyProtect" xml:space="preserve">
|
||||
<value>Legacy TUN Protect</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||
</data>
|
||||
<data name="TbCamouflageDomain" xml:space="preserve">
|
||||
<value>Camouflage domain</value>
|
||||
</data>
|
||||
@@ -1713,4 +1704,31 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
||||
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
|
||||
</data>
|
||||
<data name="menuUdpTestServer" xml:space="preserve">
|
||||
<value>Test Configurations UDP Delay</value>
|
||||
</data>
|
||||
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
|
||||
<value>UDP Test Url</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThrough" xml:space="preserve">
|
||||
<value>Local outbound address (SendThrough)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||
</data>
|
||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
||||
<value>Please fill in the correct IPv4 address for SendThrough.</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||
<value>Bind Interface</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems and TUN mode</value>
|
||||
</data>
|
||||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||
<value>Export v2rayN Internal Share Link to Clipboard</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -720,9 +720,6 @@
|
||||
<data name="TbSettingsPass" xml:space="preserve">
|
||||
<value>Mot de passe d’authentification</value>
|
||||
</data>
|
||||
<data name="TbSettingsRemoteDNS" xml:space="preserve">
|
||||
<value>DNS perso (plusieurs configurables, séparés par virgules)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSetUWP" xml:space="preserve">
|
||||
<value>Lever la restriction de proxy en boucle locale pour les applications Win10 UWP</value>
|
||||
</data>
|
||||
@@ -1062,7 +1059,7 @@
|
||||
<data name="TbSettingsTunStack" xml:space="preserve">
|
||||
<value>Pile de protocoles</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunMtu" xml:space="preserve">
|
||||
<data name="TbMtu" xml:space="preserve">
|
||||
<value>MTU</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
|
||||
@@ -1075,7 +1072,7 @@
|
||||
<value>PrivateKey</value>
|
||||
</data>
|
||||
<data name="TbReserved" xml:space="preserve">
|
||||
<value>Reserved (2,3,4)</value>
|
||||
<value>Reserved</value>
|
||||
</data>
|
||||
<data name="TbLocalAddress" xml:space="preserve">
|
||||
<value>Address (IPv4,IPv6)</value>
|
||||
@@ -1314,15 +1311,6 @@
|
||||
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThrough" xml:space="preserve">
|
||||
<value>Adresse sortante locale (SendThrough)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale.</value>
|
||||
</data>
|
||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
||||
<value>Veuillez saisir l’adresse IPv4 correcte de SendThrough.</value>
|
||||
</data>
|
||||
<data name="TransportHeaderType5" xml:space="preserve">
|
||||
<value>Mode XHTTP</value>
|
||||
</data>
|
||||
@@ -1590,9 +1578,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbEchConfigList" xml:space="preserve">
|
||||
<value>EchConfigList</value>
|
||||
</data>
|
||||
<data name="TbEchForceQuery" xml:space="preserve">
|
||||
<value>EchForceQuery</value>
|
||||
</data>
|
||||
<data name="TbFullCertTips" xml:space="preserve">
|
||||
<value>Certificat complet (chaîne), format PEM</value>
|
||||
</data>
|
||||
@@ -1716,4 +1701,31 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
||||
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThrough" xml:space="preserve">
|
||||
<value>Adresse sortante locale (SendThrough)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>Pour environnements multi-interfaces, entrez l'adresse IPv4 de la machine locale</value>
|
||||
</data>
|
||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
||||
<value>Veuillez saisir l’adresse IPv4 correcte de SendThrough.</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||
<value>Lier l'interface</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
||||
<value>Pour les environnements multi-interfaces, entrez le nom de l'interface à lier. Ne fonctionne que sur les systèmes Windows et en mode TUN</value>
|
||||
</data>
|
||||
<data name="menuUdpTestServer" xml:space="preserve">
|
||||
<value>Test Configurations UDP Delay</value>
|
||||
</data>
|
||||
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
|
||||
<value>UDP Test Url</value>
|
||||
</data>
|
||||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||
<value>Export v2rayN Internal Share Link to Clipboard</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -720,9 +720,6 @@
|
||||
<data name="TbSettingsPass" xml:space="preserve">
|
||||
<value>Hitelesítési jelszó</value>
|
||||
</data>
|
||||
<data name="TbSettingsRemoteDNS" xml:space="preserve">
|
||||
<value>Egyéni DNS (több, vesszővel (,) elválasztva)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSetUWP" xml:space="preserve">
|
||||
<value>Win10 UWP Loopback beállítása</value>
|
||||
</data>
|
||||
@@ -1065,7 +1062,7 @@
|
||||
<data name="TbSettingsTunStack" xml:space="preserve">
|
||||
<value>Hálózati verem</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunMtu" xml:space="preserve">
|
||||
<data name="TbMtu" xml:space="preserve">
|
||||
<value>MTU</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
|
||||
@@ -1078,7 +1075,7 @@
|
||||
<value>Privát kulcs</value>
|
||||
</data>
|
||||
<data name="TbReserved" xml:space="preserve">
|
||||
<value>Fenntartott (2,3,4)</value>
|
||||
<value>Fenntartott</value>
|
||||
</data>
|
||||
<data name="TbLocalAddress" xml:space="preserve">
|
||||
<value>Cím (IPv4, IPv6)</value>
|
||||
@@ -1584,9 +1581,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbEchConfigList" xml:space="preserve">
|
||||
<value>EchConfigList</value>
|
||||
</data>
|
||||
<data name="TbEchForceQuery" xml:space="preserve">
|
||||
<value>EchForceQuery</value>
|
||||
</data>
|
||||
<data name="TbFullCertTips" xml:space="preserve">
|
||||
<value>Full certificate (chain), PEM format</value>
|
||||
</data>
|
||||
@@ -1695,9 +1689,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbLegacyProtect" xml:space="preserve">
|
||||
<value>Legacy TUN Protect</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||
</data>
|
||||
<data name="TbCamouflageDomain" xml:space="preserve">
|
||||
<value>Álcázási tartomány</value>
|
||||
</data>
|
||||
@@ -1713,4 +1704,31 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
||||
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
|
||||
</data>
|
||||
<data name="menuUdpTestServer" xml:space="preserve">
|
||||
<value>Test Configurations UDP Delay</value>
|
||||
</data>
|
||||
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
|
||||
<value>UDP Test Url</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThrough" xml:space="preserve">
|
||||
<value>Local outbound address (SendThrough)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||
</data>
|
||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
||||
<value>Please fill in the correct IPv4 address for SendThrough.</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||
<value>Bind Interface</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems and TUN mode</value>
|
||||
</data>
|
||||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||
<value>Export v2rayN Internal Share Link to Clipboard</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -121,7 +121,7 @@
|
||||
<value>Export share link to clipboard successfully</value>
|
||||
</data>
|
||||
<data name="CheckServerSettings" xml:space="preserve">
|
||||
<value>Please check the Configuration settings first.</value>
|
||||
<value>Invalid configuration, please check or reselect</value>
|
||||
</data>
|
||||
<data name="ConfigurationFormatIncorrect" xml:space="preserve">
|
||||
<value>Invalid configuration format.</value>
|
||||
@@ -720,9 +720,6 @@
|
||||
<data name="TbSettingsPass" xml:space="preserve">
|
||||
<value>Auth pass</value>
|
||||
</data>
|
||||
<data name="TbSettingsRemoteDNS" xml:space="preserve">
|
||||
<value>Custom DNS (multiple, separated by commas (,))</value>
|
||||
</data>
|
||||
<data name="TbSettingsSetUWP" xml:space="preserve">
|
||||
<value>Set Win10 UWP Loopback</value>
|
||||
</data>
|
||||
@@ -862,7 +859,7 @@
|
||||
<value>Rule object Doc</value>
|
||||
</data>
|
||||
<data name="TbDnsObjectDoc" xml:space="preserve">
|
||||
<value>Supports DNS Object; Click to view documentation</value>
|
||||
<value>Please fill in DNS Object; Click to view documentation</value>
|
||||
</data>
|
||||
<data name="SubUrlTips" xml:space="preserve">
|
||||
<value>For group please leave blank here</value>
|
||||
@@ -1065,7 +1062,7 @@
|
||||
<data name="TbSettingsTunStack" xml:space="preserve">
|
||||
<value>Stack</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunMtu" xml:space="preserve">
|
||||
<data name="TbMtu" xml:space="preserve">
|
||||
<value>MTU</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
|
||||
@@ -1078,7 +1075,7 @@
|
||||
<value>Private Key</value>
|
||||
</data>
|
||||
<data name="TbReserved" xml:space="preserve">
|
||||
<value>Reserved (2,3,4)</value>
|
||||
<value>Reserved</value>
|
||||
</data>
|
||||
<data name="TbLocalAddress" xml:space="preserve">
|
||||
<value>Address (IPv4, IPv6)</value>
|
||||
@@ -1317,15 +1314,6 @@
|
||||
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThrough" xml:space="preserve">
|
||||
<value>Local outbound address (SendThrough)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||
</data>
|
||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
||||
<value>Please fill in the correct IPv4 address for SendThrough.</value>
|
||||
</data>
|
||||
<data name="TransportHeaderType5" xml:space="preserve">
|
||||
<value>xhttp mode</value>
|
||||
</data>
|
||||
@@ -1593,9 +1581,6 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbEchConfigList" xml:space="preserve">
|
||||
<value>EchConfigList</value>
|
||||
</data>
|
||||
<data name="TbEchForceQuery" xml:space="preserve">
|
||||
<value>EchForceQuery</value>
|
||||
</data>
|
||||
<data name="TbFullCertTips" xml:space="preserve">
|
||||
<value>Full certificate (chain), PEM format</value>
|
||||
</data>
|
||||
@@ -1719,4 +1704,31 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
||||
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
|
||||
</data>
|
||||
<data name="menuUdpTestServer" xml:space="preserve">
|
||||
<value>Test Configurations UDP Delay</value>
|
||||
</data>
|
||||
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
|
||||
<value>UDP Test Url</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThrough" xml:space="preserve">
|
||||
<value>Local outbound address (SendThrough)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||
</data>
|
||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
||||
<value>Please fill in the correct IPv4 address for SendThrough.</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||
<value>Bind Interface</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the name of the interface to bind. Only effective on Windows systems and TUN mode</value>
|
||||
</data>
|
||||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||
<value>Export v2rayN Internal Share Link to Clipboard</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -673,7 +673,7 @@
|
||||
<value>Ядро: базовые настройки</value>
|
||||
</data>
|
||||
<data name="TbCustomDnsRay" xml:space="preserve">
|
||||
<value>Пользовательский DNS для V2ray</value>
|
||||
<value>Пользовательский DNS для v2ray</value>
|
||||
</data>
|
||||
<data name="TbSettingsCoreKcp" xml:space="preserve">
|
||||
<value>Ядро: настройки KCP</value>
|
||||
@@ -720,9 +720,6 @@
|
||||
<data name="TbSettingsPass" xml:space="preserve">
|
||||
<value>Пароль авторизации</value>
|
||||
</data>
|
||||
<data name="TbSettingsRemoteDNS" xml:space="preserve">
|
||||
<value>Пользовательский DNS (если несколько, то делите запятыми (,))</value>
|
||||
</data>
|
||||
<data name="TbSettingsSetUWP" xml:space="preserve">
|
||||
<value>Разрешить loopback для приложений UWP (Win10)</value>
|
||||
</data>
|
||||
@@ -934,7 +931,7 @@
|
||||
<value>User-Agent</value>
|
||||
</data>
|
||||
<data name="TbSettingsDefUserAgentTips" xml:space="preserve">
|
||||
<value>This parameter is valid only for raw/http, ws, gRPC and xhttp</value>
|
||||
<value>Параметр действует только для raw/http, ws, gRPC и xhttp</value>
|
||||
</data>
|
||||
<data name="TbSettingsCurrentFontFamily" xml:space="preserve">
|
||||
<value>Шрифт (требуется перезапуск)</value>
|
||||
@@ -1065,7 +1062,7 @@
|
||||
<data name="TbSettingsTunStack" xml:space="preserve">
|
||||
<value>Сетевой стек</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunMtu" xml:space="preserve">
|
||||
<data name="TbMtu" xml:space="preserve">
|
||||
<value>MTU</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
|
||||
@@ -1078,7 +1075,7 @@
|
||||
<value>Приватный ключ</value>
|
||||
</data>
|
||||
<data name="TbReserved" xml:space="preserve">
|
||||
<value>Зарезервировано (2, 3, 4)</value>
|
||||
<value>Зарезервировано</value>
|
||||
</data>
|
||||
<data name="TbLocalAddress" xml:space="preserve">
|
||||
<value>Адрес (IPv4, IPv6)</value>
|
||||
@@ -1321,7 +1318,7 @@
|
||||
<value>XHTTP-режим</value>
|
||||
</data>
|
||||
<data name="TransportExtraTip" xml:space="preserve">
|
||||
<value>Raw JSON, format: { XHTTP Object }</value>
|
||||
<value>Сырой JSON, формат: { XHTTP Object }</value>
|
||||
</data>
|
||||
<data name="TbSettingsHide2TrayWhenClose" xml:space="preserve">
|
||||
<value>Сворачивать в трей при закрытии окна</value>
|
||||
@@ -1339,7 +1336,7 @@
|
||||
<value>Включить второй смешанный порт</value>
|
||||
</data>
|
||||
<data name="TbRoutingInboundTagTips" xml:space="preserve">
|
||||
<value>socks: локальный порт, socks2: второй локальный порт, socks3: LAN порт</value>
|
||||
<value>socks: локальный порт, socks2: второй локальный порт, socks3: LAN-порт</value>
|
||||
</data>
|
||||
<data name="TbSettingsTheme" xml:space="preserve">
|
||||
<value>Тема</value>
|
||||
@@ -1381,7 +1378,7 @@
|
||||
<value>Mldsa65Verify</value>
|
||||
</data>
|
||||
<data name="menuAddAnytlsServer" xml:space="preserve">
|
||||
<value>Добавить сервер [Anytls]</value>
|
||||
<value>Добавить сервер [AnyTLS]</value>
|
||||
</data>
|
||||
<data name="TbRemoteDNS" xml:space="preserve">
|
||||
<value>Удалённый DNS</value>
|
||||
@@ -1584,9 +1581,6 @@
|
||||
<data name="TbEchConfigList" xml:space="preserve">
|
||||
<value>EchConfigList</value>
|
||||
</data>
|
||||
<data name="TbEchForceQuery" xml:space="preserve">
|
||||
<value>EchForceQuery</value>
|
||||
</data>
|
||||
<data name="TbFullCertTips" xml:space="preserve">
|
||||
<value>Полный сертификат (цепочка) в формате PEM</value>
|
||||
</data>
|
||||
@@ -1695,22 +1689,46 @@
|
||||
<data name="TbLegacyProtect" xml:space="preserve">
|
||||
<value>Устаревшая защита TUN (Legacy Protect)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||
</data>
|
||||
<data name="TbCamouflageDomain" xml:space="preserve">
|
||||
<value>Камуфляжный домен</value>
|
||||
</data>
|
||||
<data name="TbHost" xml:space="preserve">
|
||||
<value>Host</value>
|
||||
<value>Хост</value>
|
||||
</data>
|
||||
<data name="TransportExtra" xml:space="preserve">
|
||||
<value>XHTTP Extra</value>
|
||||
<value>Дополнительные параметры XHTTP (Extra)</value>
|
||||
</data>
|
||||
<data name="TbAllowInsecureCertFetch" xml:space="preserve">
|
||||
<value>Allow insecure cert fetch (self-signed)</value>
|
||||
<value>Разрешить небезопасную загрузку сертификата (самоподписанного)</value>
|
||||
</data>
|
||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
||||
<value>Only for fetching self-signed certificates. This may expose you to MITM risks.</value>
|
||||
<value>Только для загрузки самоподписанных сертификатов. Это может подвергнуть вас риску атаки «человек посередине» (MITM).</value>
|
||||
</data>
|
||||
<data name="menuUdpTestServer" xml:space="preserve">
|
||||
<value>Тест UDP-задержки конфигураций</value>
|
||||
</data>
|
||||
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
|
||||
<value>URL для UDP-теста</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThrough" xml:space="preserve">
|
||||
<value>Локальный исходящий адрес (SendThrough)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>Для среды с несколькими сетевыми интерфейсами укажите IPv4-адрес локального компьютера</value>
|
||||
</data>
|
||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
||||
<value>Укажите корректный IPv4-адрес для SendThrough.</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||
<value>Привязать интерфейс</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
||||
<value>Для среды с несколькими сетевыми интерфейсами укажите имя интерфейса для привязки. Работает только в Windows и режиме TUN</value>
|
||||
</data>
|
||||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||
<value>Export v2rayN Internal Share Link to Clipboard</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -121,7 +121,7 @@
|
||||
<value>导出分享链接至剪贴板成功</value>
|
||||
</data>
|
||||
<data name="CheckServerSettings" xml:space="preserve">
|
||||
<value>请先检查设置</value>
|
||||
<value>配置项无效,请检查或重新选择</value>
|
||||
</data>
|
||||
<data name="ConfigurationFormatIncorrect" xml:space="preserve">
|
||||
<value>配置格式不正确</value>
|
||||
@@ -720,9 +720,6 @@
|
||||
<data name="TbSettingsPass" xml:space="preserve">
|
||||
<value>认证密码</value>
|
||||
</data>
|
||||
<data name="TbSettingsRemoteDNS" xml:space="preserve">
|
||||
<value>自定义 DNS (可多个,用逗号 (,) 分隔)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSetUWP" xml:space="preserve">
|
||||
<value>解除 Win10 UWP 应用回环代理限制</value>
|
||||
</data>
|
||||
@@ -862,7 +859,7 @@
|
||||
<value>规则详细说明文档</value>
|
||||
</data>
|
||||
<data name="TbDnsObjectDoc" xml:space="preserve">
|
||||
<value>支持填写 DnsObject,JSON 格式,点击查看文档</value>
|
||||
<value>请填写 DnsObject,JSON 格式,点击查看文档</value>
|
||||
</data>
|
||||
<data name="SubUrlTips" xml:space="preserve">
|
||||
<value>普通分组此处请留空</value>
|
||||
@@ -1062,7 +1059,7 @@
|
||||
<data name="TbSettingsTunStack" xml:space="preserve">
|
||||
<value>协议栈</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunMtu" xml:space="preserve">
|
||||
<data name="TbMtu" xml:space="preserve">
|
||||
<value>MTU</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
|
||||
@@ -1075,7 +1072,7 @@
|
||||
<value>PrivateKey</value>
|
||||
</data>
|
||||
<data name="TbReserved" xml:space="preserve">
|
||||
<value>Reserved (2,3,4)</value>
|
||||
<value>Reserved</value>
|
||||
</data>
|
||||
<data name="TbLocalAddress" xml:space="preserve">
|
||||
<value>Address (IPv4,IPv6)</value>
|
||||
@@ -1314,15 +1311,6 @@
|
||||
<data name="TbSettingsLinuxSudoPasswordTip" xml:space="preserve">
|
||||
<value>密码将调用命令行校验,如果因为校验错误导致无法正常运行时,请重启本应用。 密码不会存储,每次重启后都需要再次输入。</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThrough" xml:space="preserve">
|
||||
<value>本地出站地址 (SendThrough)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>用于多网口环境,请填写本机 IPv4 地址</value>
|
||||
</data>
|
||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
||||
<value>请填写正确的 SendThrough IPv4 地址。</value>
|
||||
</data>
|
||||
<data name="TransportHeaderType5" xml:space="preserve">
|
||||
<value>XHTTP 模式</value>
|
||||
</data>
|
||||
@@ -1590,9 +1578,6 @@
|
||||
<data name="TbEchConfigList" xml:space="preserve">
|
||||
<value>EchConfigList</value>
|
||||
</data>
|
||||
<data name="TbEchForceQuery" xml:space="preserve">
|
||||
<value>EchForceQuery</value>
|
||||
</data>
|
||||
<data name="TbFullCertTips" xml:space="preserve">
|
||||
<value>完整证书(链),PEM 格式</value>
|
||||
</data>
|
||||
@@ -1716,4 +1701,31 @@
|
||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
||||
<value>仅用于抓取自签证书,存在中间人风险。</value>
|
||||
</data>
|
||||
<data name="menuUdpTestServer" xml:space="preserve">
|
||||
<value>测试 UDP 延迟 (多选)</value>
|
||||
</data>
|
||||
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
|
||||
<value>UDP 测试地址</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThrough" xml:space="preserve">
|
||||
<value>本地出站地址 (SendThrough)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>用于多网口环境,请填写本机 IPv4 地址</value>
|
||||
</data>
|
||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
||||
<value>请填写正确的 SendThrough IPv4 地址。</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||
<value>绑定网口</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
||||
<value>用于多网口环境,填写要绑定的网口名称,仅生效于 Windows 系统和 TUN 模式</value>
|
||||
</data>
|
||||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||
<value>导出 v2rayN 内部分享链接至剪贴板 (多选)</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -121,7 +121,7 @@
|
||||
<value>匯出分享連結至剪貼簿成功</value>
|
||||
</data>
|
||||
<data name="CheckServerSettings" xml:space="preserve">
|
||||
<value>請先檢查設定</value>
|
||||
<value>配置項目無效,請檢查或重新選擇</value>
|
||||
</data>
|
||||
<data name="ConfigurationFormatIncorrect" xml:space="preserve">
|
||||
<value>設定格式不正確</value>
|
||||
@@ -720,9 +720,6 @@
|
||||
<data name="TbSettingsPass" xml:space="preserve">
|
||||
<value>認證密碼</value>
|
||||
</data>
|
||||
<data name="TbSettingsRemoteDNS" xml:space="preserve">
|
||||
<value>自訂 DNS (可多個,用逗號 (,) 分隔)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSetUWP" xml:space="preserve">
|
||||
<value>解除 Win10 UWP 應用回環代理限制</value>
|
||||
</data>
|
||||
@@ -862,7 +859,7 @@
|
||||
<value>規則詳細說明檔案</value>
|
||||
</data>
|
||||
<data name="TbDnsObjectDoc" xml:space="preserve">
|
||||
<value>支援填寫 DnsObject,JSON 格式,點擊查看說明</value>
|
||||
<value>請填寫 DnsObject,JSON 格式,點擊查看說明</value>
|
||||
</data>
|
||||
<data name="SubUrlTips" xml:space="preserve">
|
||||
<value>普通分組此處請留空</value>
|
||||
@@ -1062,7 +1059,7 @@
|
||||
<data name="TbSettingsTunStack" xml:space="preserve">
|
||||
<value>協定堆疊</value>
|
||||
</data>
|
||||
<data name="TbSettingsTunMtu" xml:space="preserve">
|
||||
<data name="TbMtu" xml:space="preserve">
|
||||
<value>MTU</value>
|
||||
</data>
|
||||
<data name="TbSettingsEnableIPv6Address" xml:space="preserve">
|
||||
@@ -1075,7 +1072,7 @@
|
||||
<value>PrivateKey</value>
|
||||
</data>
|
||||
<data name="TbReserved" xml:space="preserve">
|
||||
<value>Reserved (2,3,4)</value>
|
||||
<value>Reserved</value>
|
||||
</data>
|
||||
<data name="TbLocalAddress" xml:space="preserve">
|
||||
<value>Address (Ipv4,Ipv6)</value>
|
||||
@@ -1581,9 +1578,6 @@
|
||||
<data name="TbEchConfigList" xml:space="preserve">
|
||||
<value>EchConfigList</value>
|
||||
</data>
|
||||
<data name="TbEchForceQuery" xml:space="preserve">
|
||||
<value>EchForceQuery</value>
|
||||
</data>
|
||||
<data name="TbFullCertTips" xml:space="preserve">
|
||||
<value>完整憑證(鏈),PEM 格式</value>
|
||||
</data>
|
||||
@@ -1690,10 +1684,7 @@
|
||||
<value>ICMP 路由策略</value>
|
||||
</data>
|
||||
<data name="TbLegacyProtect" xml:space="preserve">
|
||||
<value>Legacy TUN Protect</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>For multi-interface environments, enter the local machine's IPv4 address</value>
|
||||
<value>舊版 TUN 保護</value>
|
||||
</data>
|
||||
<data name="TbCamouflageDomain" xml:space="preserve">
|
||||
<value>偽裝域名</value>
|
||||
@@ -1710,4 +1701,31 @@
|
||||
<data name="TbAllowInsecureCertFetchTips" xml:space="preserve">
|
||||
<value>僅用於抓取自簽證書,存在中間人風險。</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="menuUdpTestServer" xml:space="preserve">
|
||||
<value>測試 UDP 延遲(多選)</value>
|
||||
</data>
|
||||
<data name="TbSettingsUdpTestUrl" xml:space="preserve">
|
||||
<value>UDP 測試網址</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThrough" xml:space="preserve">
|
||||
<value>本機出站位址 (SendThrough)</value>
|
||||
</data>
|
||||
<data name="TbSettingsSendThroughTip" xml:space="preserve">
|
||||
<value>適用於多網路介面環境,請填寫本機 IPv4 位址</value>
|
||||
</data>
|
||||
<data name="FillCorrectSendThroughIPv4" xml:space="preserve">
|
||||
<value>請填寫正確的 SendThrough IPv4 位址。</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterface" xml:space="preserve">
|
||||
<value>綁定網路介面</value>
|
||||
</data>
|
||||
<data name="TbSettingsBindInterfaceTip" xml:space="preserve">
|
||||
<value>適用於多網路介面環境,請填寫要綁定的介面名稱;Windows 系統有效,其他系統僅在 TUN 模式下生效。</value>
|
||||
</data>
|
||||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport2InnerUri" xml:space="preserve">
|
||||
<value>匯出 v2rayN 內部分享連結至剪貼簿(多選)</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -84,4 +84,8 @@
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ServiceLib.UdpTest\ServiceLib.UdpTest.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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, "");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -298,7 +298,7 @@ public partial class CoreConfigSingboxService
|
||||
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
|
||||
var expectedIPCidr = new List<string>();
|
||||
var expectedIPsRegions = new List<string>();
|
||||
var regionNames = new HashSet<string>();
|
||||
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<string>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
@@ -62,7 +62,7 @@ public partial class CoreConfigSingboxService
|
||||
}
|
||||
|
||||
var tunInbound = JsonUtils.Deserialize<Inbound4Sbox>(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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, "");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -121,7 +121,7 @@ public partial class CoreConfigV2rayService
|
||||
var proxyGeositeList = new List<string>();
|
||||
var expectedDomainList = new List<string>();
|
||||
var expectedIPs = new List<string>();
|
||||
var regionNames = new HashSet<string>();
|
||||
var regionName = string.Empty;
|
||||
|
||||
var bootstrapDNSAddress = ParseDnsAddresses(simpleDNSItem?.BootstrapDNS, Global.DomainPureIPDNSAddress.First());
|
||||
var dnsServerDomains = new List<string>();
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -54,12 +54,17 @@ public partial class CoreConfigV2rayService
|
||||
_config.TunModeItem.Mtu = Global.TunMtus.First();
|
||||
}
|
||||
var tunInbound = JsonUtils.Deserialize<Inbounds4Ray>(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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using ServiceLib.UdpTest;
|
||||
|
||||
namespace ServiceLib.Services;
|
||||
|
||||
public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateFunc)
|
||||
@@ -49,6 +51,10 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> 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<SpeedTestResult, Task> 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<SpeedTestResult, Task> updateF
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task RunUdpTestBatchAsync(List<ServerTestItem> lstSelected, string exitLoopKey, int pageSize = 0)
|
||||
{
|
||||
if (pageSize <= 0)
|
||||
{
|
||||
pageSize = lstSelected.Count < Global.SpeedTestPageSize ? lstSelected.Count : Global.SpeedTestPageSize;
|
||||
}
|
||||
var lstTest = GetTestBatchItem(lstSelected, pageSize);
|
||||
|
||||
List<ServerTestItem> 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<bool> RunUdpTestAsync(List<ServerTestItem> selecteds, string exitLoopKey)
|
||||
{
|
||||
ProcessService processService = null;
|
||||
try
|
||||
{
|
||||
processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds);
|
||||
if (processService is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
await Task.Delay(1000);
|
||||
|
||||
List<Task> 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<ServerTestItem> selecteds, int concurrencyCount, bool blSpeedTest, string exitLoopKey)
|
||||
{
|
||||
using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount);
|
||||
@@ -330,6 +417,24 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<int> 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<int> GetTcpingTime(string url, int port)
|
||||
{
|
||||
var responseTime = -1;
|
||||
|
||||
@@ -299,7 +299,6 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
else if (Utils.IsLinux())
|
||||
{
|
||||
var arch = RuntimeInformation.ProcessArchitecture;
|
||||
@@ -314,7 +313,6 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
else if (Utils.IsMacOS())
|
||||
{
|
||||
return RuntimeInformation.ProcessArchitecture switch
|
||||
@@ -377,8 +375,8 @@ public class UpdateService(Config config, Func<bool, string, Task> updateFunc)
|
||||
var rules = JsonUtils.Deserialize<List<RulesItem>>(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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -21,6 +21,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; }
|
||||
@@ -59,6 +60,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; }
|
||||
@@ -155,7 +157,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;
|
||||
@@ -195,6 +198,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;
|
||||
@@ -299,7 +303,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);
|
||||
@@ -344,7 +348,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;
|
||||
@@ -368,6 +373,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;
|
||||
|
||||
@@ -60,6 +60,7 @@ public class ProfilesViewModel : MyReactiveObject
|
||||
|
||||
public ReactiveCommand<Unit, Unit> TcpingServerCmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> RealPingServerCmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> UdpTestServerCmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> SpeedServerCmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> SortServerResultCmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> RemoveInvalidServerResultCmd { get; }
|
||||
@@ -71,6 +72,7 @@ public class ProfilesViewModel : MyReactiveObject
|
||||
public ReactiveCommand<Unit, Unit> Export2ClientConfigClipboardCmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> Export2ShareUrlCmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> Export2ShareUrlBase64Cmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> Export2InnerUriCmd { get; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> AddSubCmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> 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
|
||||
|
||||
@@ -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">
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
@@ -542,43 +542,57 @@
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbReserved}" />
|
||||
Text="{x:Static resx:ResUI.TbPreSharedKey}" />
|
||||
<TextBox
|
||||
x:Name="txtPath9"
|
||||
x:Name="txtPreSharedKey9"
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Watermark="2,3,4" />
|
||||
HorizontalAlignment="Left" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbReserved}" />
|
||||
<TextBox
|
||||
x:Name="txtPath9"
|
||||
Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Watermark="0, 0, 0" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="5"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbLocalAddress}" />
|
||||
<TextBox
|
||||
x:Name="txtRequestHost9"
|
||||
Grid.Row="4"
|
||||
Grid.Row="5"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Watermark="Ipv4,Ipv6" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="5"
|
||||
Grid.Row="6"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsTunMtu}" />
|
||||
Text="{x:Static resx:ResUI.TbMtu}" />
|
||||
<TextBox
|
||||
x:Name="txtShortId9"
|
||||
Grid.Row="5"
|
||||
Grid.Row="6"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left"
|
||||
Watermark="1500" />
|
||||
Watermark="1280" />
|
||||
</Grid>
|
||||
<Grid
|
||||
x:Name="gridAnytls"
|
||||
@@ -831,7 +845,7 @@
|
||||
x:Name="gridTransportKcp"
|
||||
ColumnDefinitions="300,Auto"
|
||||
IsVisible="False"
|
||||
RowDefinitions="Auto,Auto">
|
||||
RowDefinitions="Auto,Auto,Auto">
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
@@ -843,19 +857,34 @@
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}" />
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbMtu}" />
|
||||
<TextBox
|
||||
x:Name="txtKcpMtu"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left" />
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="Seed" />
|
||||
<TextBox
|
||||
x:Name="txtKcpSeed"
|
||||
Grid.Row="1"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}" />
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left" />
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
@@ -1100,19 +1129,6 @@
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="6"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbEchForceQuery}" />
|
||||
<ComboBox
|
||||
x:Name="cmbEchForceQuery"
|
||||
Grid.Row="6"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="7"
|
||||
Grid.Column="0"
|
||||
|
||||
@@ -39,11 +39,8 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
|
||||
cmbFingerprint2.ItemsSource = Global.Fingerprints;
|
||||
cmbAllowInsecure.ItemsSource = Global.AllowInsecure;
|
||||
cmbAlpn.ItemsSource = Global.Alpns;
|
||||
cmbEchForceQuery.ItemsSource = Global.EchForceQuerys;
|
||||
|
||||
var lstStreamSecurity = new List<string>();
|
||||
lstStreamSecurity.Add(string.Empty);
|
||||
lstStreamSecurity.Add(Global.StreamSecurity);
|
||||
var lstStreamSecurity = new List<string> { string.Empty, Global.StreamSecurity };
|
||||
|
||||
switch (profileItem.ConfigType)
|
||||
{
|
||||
@@ -79,7 +76,7 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
|
||||
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<AddServerViewModel>
|
||||
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<AddServerViewModel>
|
||||
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<AddServerViewModel>
|
||||
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<AddServerViewModel>
|
||||
|
||||
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<AddServerViewModel>
|
||||
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<AddServerViewModel>
|
||||
case nameof(ETransport.raw):
|
||||
gridTransportRaw.IsVisible = true;
|
||||
break;
|
||||
|
||||
case nameof(ETransport.kcp):
|
||||
gridTransportKcp.IsVisible = true;
|
||||
break;
|
||||
|
||||
case nameof(ETransport.ws):
|
||||
gridTransportWs.IsVisible = true;
|
||||
break;
|
||||
|
||||
case nameof(ETransport.httpupgrade):
|
||||
gridTransportHttpupgrade.IsVisible = true;
|
||||
break;
|
||||
|
||||
case nameof(ETransport.xhttp):
|
||||
gridTransportXhttp.IsVisible = true;
|
||||
break;
|
||||
|
||||
case nameof(ETransport.grpc):
|
||||
gridTransportGrpc.IsVisible = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
gridTransportRaw.IsVisible = true;
|
||||
break;
|
||||
|
||||
@@ -342,11 +342,6 @@
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsRemoteDNS}" />
|
||||
|
||||
<TextBlock Margin="{StaticResource Margin4}" VerticalAlignment="Center">
|
||||
<HyperlinkButton Classes="WithIcon" Click="linkDnsObjectDoc_Click">
|
||||
<TextBlock Text="{x:Static resx:ResUI.TbDnsObjectDoc}" />
|
||||
@@ -494,7 +489,6 @@
|
||||
Header="{x:Static resx:ResUI.TbSettingsTunMode}">
|
||||
<local:JsonEditor Name="txttunDNS2Compatible" VerticalAlignment="Stretch" />
|
||||
</HeaderedContentControl>
|
||||
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
<ScrollViewer VerticalScrollBarVisibility="Visible">
|
||||
<Grid
|
||||
Margin="{StaticResource Margin4}"
|
||||
ColumnDefinitions="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">
|
||||
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">
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
@@ -331,16 +331,36 @@
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsBindInterface}" />
|
||||
<TextBox
|
||||
x:Name="txtbindInterface"
|
||||
Grid.Row="21"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}" />
|
||||
<TextBlock
|
||||
Grid.Row="21"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsBindInterfaceTip}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="22"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsSendThrough}" />
|
||||
<TextBox
|
||||
x:Name="txtsendThrough"
|
||||
Grid.Row="21"
|
||||
Grid.Row="22"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Watermark="0.0.0.0" />
|
||||
<TextBlock
|
||||
Grid.Row="21"
|
||||
Grid.Row="22"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -355,7 +375,7 @@
|
||||
<Grid
|
||||
Margin="{StaticResource Margin4}"
|
||||
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,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">
|
||||
|
||||
<TextBlock
|
||||
x:Name="tbAutoRun"
|
||||
@@ -418,7 +438,7 @@
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left" />
|
||||
|
||||
<TextBlock
|
||||
<!--<TextBlock
|
||||
Grid.Row="5"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
@@ -429,7 +449,7 @@
|
||||
Grid.Row="5"
|
||||
Grid.Column="1"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left" />
|
||||
HorizontalAlignment="Left" />-->
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="8"
|
||||
@@ -588,57 +608,71 @@
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsUdpTestUrl}" />
|
||||
<ComboBox
|
||||
x:Name="cmbUdpTestTarget"
|
||||
Grid.Row="20"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
IsEditable="True" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="22"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsIPAPIUrl}" />
|
||||
<ComboBox
|
||||
x:Name="cmbIPAPIUrl"
|
||||
Grid.Row="20"
|
||||
Grid.Row="22"
|
||||
Grid.Column="1"
|
||||
Width="300"
|
||||
Margin="{StaticResource Margin4}"
|
||||
IsEditable="True" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="21"
|
||||
Grid.Row="23"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsSubConvert}" />
|
||||
<ComboBox
|
||||
x:Name="cmbSubConvertUrl"
|
||||
Grid.Row="21"
|
||||
Grid.Row="23"
|
||||
Grid.Column="1"
|
||||
Width="300"
|
||||
Margin="{StaticResource Margin4}"
|
||||
IsEditable="True" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="22"
|
||||
Grid.Row="24"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsMainGirdOrientation}" />
|
||||
<ComboBox
|
||||
x:Name="cmbMainGirdOrientation"
|
||||
Grid.Row="22"
|
||||
Grid.Row="24"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="23"
|
||||
Grid.Row="25"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsGeoFilesSource}" />
|
||||
<ComboBox
|
||||
x:Name="cmbGetFilesSourceUrl"
|
||||
Grid.Row="23"
|
||||
Grid.Row="25"
|
||||
Grid.Column="1"
|
||||
Width="300"
|
||||
Margin="{StaticResource Margin4}"
|
||||
IsEditable="True" />
|
||||
<TextBlock
|
||||
Grid.Row="23"
|
||||
Grid.Row="25"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -646,20 +680,20 @@
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="24"
|
||||
Grid.Row="26"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsSrsFilesSource}" />
|
||||
<ComboBox
|
||||
x:Name="cmbSrsFilesSourceUrl"
|
||||
Grid.Row="24"
|
||||
Grid.Row="26"
|
||||
Grid.Column="1"
|
||||
Width="300"
|
||||
Margin="{StaticResource Margin4}"
|
||||
IsEditable="True" />
|
||||
<TextBlock
|
||||
Grid.Row="24"
|
||||
Grid.Row="26"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -667,26 +701,25 @@
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="25"
|
||||
Grid.Row="27"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsRoutingRulesSource}" />
|
||||
<ComboBox
|
||||
x:Name="cmbRoutingRulesSourceUrl"
|
||||
Grid.Row="25"
|
||||
Grid.Row="27"
|
||||
Grid.Column="1"
|
||||
Width="300"
|
||||
Margin="{StaticResource Margin4}"
|
||||
IsEditable="True" />
|
||||
<TextBlock
|
||||
Grid.Row="25"
|
||||
Grid.Row="27"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsChinaUserTip}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
@@ -836,7 +869,7 @@
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbSettingsTunMtu}" />
|
||||
Text="{x:Static resx:ResUI.TbMtu}" />
|
||||
<ComboBox
|
||||
x:Name="cmbMtu"
|
||||
Grid.Row="5"
|
||||
|
||||
@@ -49,6 +49,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
|
||||
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;
|
||||
@@ -76,6 +77,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
|
||||
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);
|
||||
@@ -87,7 +89,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
|
||||
this.Bind(ViewModel, vm => vm.EnableStatistics, v => v.togEnableStatistics.IsChecked).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.DisplayRealTimeSpeed, v => v.togDisplayRealTimeSpeed.IsChecked).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.KeepOlderDedupl, v => v.togKeepOlderDedupl.IsChecked).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.EnableAutoAdjustMainLvColWidth, v => v.togEnableAutoAdjustMainLvColWidth.IsChecked).DisposeWith(disposables);
|
||||
//this.Bind(ViewModel, vm => vm.EnableAutoAdjustMainLvColWidth, v => v.togEnableAutoAdjustMainLvColWidth.IsChecked).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.AutoHideStartup, v => v.togAutoHideStartup.IsChecked).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Hide2TrayWhenClose, v => v.togHide2TrayWhenClose.IsChecked).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.MacOSShowInDock, v => v.togMacOSShowInDock.IsChecked).DisposeWith(disposables);
|
||||
@@ -97,6 +99,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
|
||||
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<OptionSettingViewModel, OptionSettingWindow, int, int>(ViewModel,
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
x:Name="menuSpeedServer"
|
||||
Header="{x:Static resx:ResUI.menuSpeedServer}"
|
||||
InputGesture="Ctrl+T" />
|
||||
<MenuItem x:Name="menuUdpTestServer" Header="{x:Static resx:ResUI.menuUdpTestServer}" />
|
||||
<MenuItem x:Name="menuSortServerResult" Header="{x:Static resx:ResUI.menuSortServerResult}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
@@ -191,6 +192,7 @@
|
||||
Header="{x:Static resx:ResUI.menuExport2ShareUrl}"
|
||||
InputGesture="Ctrl+C" />
|
||||
<MenuItem x:Name="menuExport2ShareUrlBase64" Header="{x:Static resx:ResUI.menuExport2ShareUrlBase64}" />
|
||||
<MenuItem x:Name="menuExport2InnerUri" Header="{x:Static resx:ResUI.menuExport2InnerUri}" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="{x:Static resx:ResUI.menuGenGroupServer}">
|
||||
|
||||
@@ -77,6 +77,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
||||
this.BindCommand(ViewModel, vm => vm.MixedTestServerCmd, v => v.menuMixedTestServer).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.TcpingServerCmd, v => v.menuTcpingServer).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.RealPingServerCmd, v => v.menuRealPingServer).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.UdpTestServerCmd, v => v.menuUdpTestServer).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.SpeedServerCmd, v => v.menuSpeedServer).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.SortServerResultCmd, v => v.menuSortServerResult).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.RemoveInvalidServerResultCmd, v => v.menuRemoveInvalidServerResult).DisposeWith(disposables);
|
||||
@@ -87,6 +88,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
||||
this.BindCommand(ViewModel, vm => vm.Export2ClientConfigClipboardCmd, v => v.menuExport2ClientConfigClipboard).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlCmd, v => v.menuExport2ShareUrl).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.Export2InnerUriCmd, v => v.menuExport2InnerUri).DisposeWith(disposables);
|
||||
|
||||
AppEvents.AppExitRequested
|
||||
.AsObservable()
|
||||
@@ -94,11 +96,11 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
||||
.Subscribe(_ => StorageUI())
|
||||
.DisposeWith(disposables);
|
||||
|
||||
AppEvents.AdjustMainLvColWidthRequested
|
||||
.AsObservable()
|
||||
.ObserveOn(RxSchedulers.MainThreadScheduler)
|
||||
.Subscribe(_ => AutofitColumnWidth())
|
||||
.DisposeWith(disposables);
|
||||
//AppEvents.AdjustMainLvColWidthRequested
|
||||
// .AsObservable()
|
||||
// .ObserveOn(RxSchedulers.MainThreadScheduler)
|
||||
// .Subscribe(_ => AutofitColumnWidth())
|
||||
// .DisposeWith(disposables);
|
||||
});
|
||||
|
||||
RestoreUI();
|
||||
|
||||
@@ -64,7 +64,10 @@ public partial class RoutingSettingWindow : WindowBase<RoutingSettingViewModel>
|
||||
|
||||
private void RoutingSettingWindow_Closing(object? sender, WindowClosingEventArgs e)
|
||||
{
|
||||
if (_closed) return;
|
||||
if (_closed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// DomainStrategy is auto-saved reactively; just ensure the caller knows changes were made
|
||||
if (ViewModel?.IsModified == true)
|
||||
|
||||
+7
-1
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.4.11620.152 stable
|
||||
VisualStudioVersion = 18.5.11709.299 stable
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "v2rayN", "v2rayN\v2rayN.csproj", "{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}"
|
||||
EndProject
|
||||
@@ -34,6 +34,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub Action", "GitHub Act
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceLib.Tests", "ServiceLib.Tests\ServiceLib.Tests.csproj", "{E0B6C5C7-ED48-42EB-947A-877779E9F555}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceLib.UdpTest", "ServiceLib.UdpTest\ServiceLib.UdpTest.csproj", "{CE9D9298-0289-4718-2522-B236489F2912}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -64,6 +66,10 @@ Global
|
||||
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CE9D9298-0289-4718-2522-B236489F2912}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CE9D9298-0289-4718-2522-B236489F2912}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CE9D9298-0289-4718-2522-B236489F2912}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CE9D9298-0289-4718-2522-B236489F2912}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<Project Path="AmazTool/AmazTool.csproj" />
|
||||
<Project Path="GlobalHotKeys/src/GlobalHotKeys/GlobalHotKeys.csproj" />
|
||||
<Project Path="ServiceLib.Tests/ServiceLib.Tests.csproj" />
|
||||
<Project Path="ServiceLib.UdpTest/ServiceLib.UdpTest.csproj" Id="77930428-4dc4-4130-9031-6b9e714f5d20" />
|
||||
<Project Path="ServiceLib/ServiceLib.csproj" />
|
||||
<Project Path="v2rayN.Desktop/v2rayN.Desktop.csproj" />
|
||||
<Project Path="v2rayN/v2rayN.csproj" />
|
||||
|
||||
@@ -137,7 +137,7 @@ public class ThemeSettingViewModel : MyReactiveObject
|
||||
|
||||
private void ModifyFontSize()
|
||||
{
|
||||
double size = (long)CurrentFontSize;
|
||||
double size = CurrentFontSize;
|
||||
if (size < Global.MinFontSize)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -415,7 +415,7 @@
|
||||
x:Name="txtSecurity5"
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
@@ -678,6 +678,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="300" />
|
||||
@@ -721,14 +722,14 @@
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbReserved}" />
|
||||
Text="{x:Static resx:ResUI.TbPreSharedKey}" />
|
||||
<TextBox
|
||||
x:Name="txtPath9"
|
||||
x:Name="txtPreSharedKey9"
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
materialDesign:HintAssist.Hint="2,3,4"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
|
||||
<TextBlock
|
||||
@@ -737,10 +738,26 @@
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbReserved}" />
|
||||
<TextBox
|
||||
x:Name="txtPath9"
|
||||
Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
materialDesign:HintAssist.Hint="0, 0, 0"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="5"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbLocalAddress}" />
|
||||
<TextBox
|
||||
x:Name="txtRequestHost9"
|
||||
Grid.Row="4"
|
||||
Grid.Row="5"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
@@ -748,20 +765,20 @@
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="5"
|
||||
Grid.Row="6"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbSettingsTunMtu}" />
|
||||
Text="{x:Static resx:ResUI.TbMtu}" />
|
||||
<TextBox
|
||||
x:Name="txtShortId9"
|
||||
Grid.Row="5"
|
||||
Grid.Row="6"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left"
|
||||
materialDesign:HintAssist.Hint="1500"
|
||||
materialDesign:HintAssist.Hint="1280"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
</Grid>
|
||||
<Grid
|
||||
@@ -1096,6 +1113,7 @@
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
@@ -1110,6 +1128,7 @@
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource DefComboBox}" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
@@ -1117,13 +1136,29 @@
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbMtu}" />
|
||||
<TextBox
|
||||
x:Name="txtKcpMtu"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="Seed" />
|
||||
<TextBox
|
||||
x:Name="txtKcpSeed"
|
||||
Grid.Row="1"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
</Grid>
|
||||
|
||||
@@ -1426,21 +1461,6 @@
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="6"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbEchForceQuery}" />
|
||||
<ComboBox
|
||||
x:Name="cmbEchForceQuery"
|
||||
Grid.Row="6"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Style="{StaticResource DefComboBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="7"
|
||||
Grid.Column="0"
|
||||
|
||||
@@ -38,11 +38,8 @@ public partial class AddServerWindow
|
||||
cmbFingerprint2.ItemsSource = Global.Fingerprints;
|
||||
cmbAllowInsecure.ItemsSource = Global.AllowInsecure;
|
||||
cmbAlpn.ItemsSource = Global.Alpns;
|
||||
cmbEchForceQuery.ItemsSource = Global.EchForceQuerys;
|
||||
|
||||
var lstStreamSecurity = new List<string>();
|
||||
lstStreamSecurity.Add(string.Empty);
|
||||
lstStreamSecurity.Add(Global.StreamSecurity);
|
||||
var lstStreamSecurity = new List<string> { string.Empty, Global.StreamSecurity };
|
||||
|
||||
switch (profileItem.ConfigType)
|
||||
{
|
||||
@@ -78,7 +75,7 @@ public partial class AddServerWindow
|
||||
sepa2.Visibility = Visibility.Collapsed;
|
||||
gridTransport.Visibility = Visibility.Collapsed;
|
||||
cmbFingerprint.IsEnabled = false;
|
||||
cmbFingerprint.Text = string.Empty;
|
||||
cmbAlpn.IsEnabled = false;
|
||||
break;
|
||||
|
||||
case EConfigType.TUIC:
|
||||
@@ -87,7 +84,6 @@ public partial class AddServerWindow
|
||||
gridTransport.Visibility = Visibility.Collapsed;
|
||||
cmbCoreType.IsEnabled = false;
|
||||
cmbFingerprint.IsEnabled = false;
|
||||
cmbFingerprint.Text = string.Empty;
|
||||
gridFinalmask.Visibility = Visibility.Collapsed;
|
||||
|
||||
cmbCongestionControl8.ItemsSource = Global.TuicCongestionControls;
|
||||
@@ -118,11 +114,8 @@ public partial class AddServerWindow
|
||||
cmbCoreType.IsEnabled = false;
|
||||
gridFinalmask.Visibility = Visibility.Collapsed;
|
||||
cmbFingerprint.IsEnabled = false;
|
||||
cmbFingerprint.Text = string.Empty;
|
||||
cmbAlpn.IsEnabled = false;
|
||||
cmbAlpn.Text = string.Empty;
|
||||
cmbAllowInsecure.IsEnabled = false;
|
||||
cmbAllowInsecure.Text = string.Empty;
|
||||
|
||||
cmbCongestionControl12.ItemsSource = Global.NaiveCongestionControls;
|
||||
break;
|
||||
@@ -191,6 +184,7 @@ public partial class AddServerWindow
|
||||
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);
|
||||
@@ -216,6 +210,7 @@ public partial class AddServerWindow
|
||||
|
||||
this.Bind(ViewModel, vm => vm.KcpHeaderType, v => v.cmbHeaderTypeKcp.Text).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);
|
||||
@@ -246,7 +241,6 @@ public partial class AddServerWindow
|
||||
.BindTo(this, v => v.txtAllowInsecureCertFetchTips.Visibility);
|
||||
this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).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.Text).DisposeWith(disposables);
|
||||
|
||||
//reality
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables);
|
||||
@@ -339,21 +333,27 @@ public partial class AddServerWindow
|
||||
case nameof(ETransport.raw):
|
||||
gridTransportRaw.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
case nameof(ETransport.kcp):
|
||||
gridTransportKcp.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
case nameof(ETransport.ws):
|
||||
gridTransportWs.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
case nameof(ETransport.httpupgrade):
|
||||
gridTransportHttpupgrade.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
case nameof(ETransport.xhttp):
|
||||
gridTransportXhttp.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
case nameof(ETransport.grpc):
|
||||
gridTransportGrpc.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
default:
|
||||
gridTransportRaw.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
@@ -390,11 +390,6 @@
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbSettingsRemoteDNS}" />
|
||||
<TextBlock
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
|
||||
@@ -170,10 +170,10 @@ public partial class MainWindow
|
||||
|
||||
private void OnProgramStarted(object state, bool timeout)
|
||||
{
|
||||
Application.Current?.Dispatcher.Invoke((Action)(() =>
|
||||
Application.Current?.Dispatcher.Invoke(() =>
|
||||
{
|
||||
ShowHideWindow(true);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private async Task DelegateSnackMsg(string content)
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@@ -398,21 +399,44 @@
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbSettingsSendThrough}" />
|
||||
Text="{x:Static resx:ResUI.TbSettingsBindInterface}" />
|
||||
<TextBox
|
||||
x:Name="txtsendThrough"
|
||||
x:Name="txtbindInterface"
|
||||
Grid.Row="21"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin8}"
|
||||
Style="{StaticResource DefTextBox}"
|
||||
materialDesign:HintAssist.Hint="0.0.0.0" />
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
<TextBlock
|
||||
Grid.Row="21"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbSettingsBindInterfaceTip}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="22"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbSettingsSendThrough}" />
|
||||
<TextBox
|
||||
x:Name="txtsendThrough"
|
||||
Grid.Row="22"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin8}"
|
||||
materialDesign:HintAssist.Hint="0.0.0.0"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
<TextBlock
|
||||
Grid.Row="22"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbSettingsSendThroughTip}"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
@@ -445,7 +469,7 @@
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbSettingsTunMtu}" />
|
||||
Text="{x:Static resx:ResUI.TbMtu}" />
|
||||
<TextBox Style="{StaticResource DefTextBox}"
|
||||
x:Name="txtKcpmtu"
|
||||
Grid.Row="1"
|
||||
@@ -569,6 +593,8 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@@ -834,10 +860,26 @@
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbSettingsUdpTestUrl}" />
|
||||
<ComboBox
|
||||
x:Name="cmbUdpTestTarget"
|
||||
Grid.Row="20"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin8}"
|
||||
IsEditable="True"
|
||||
Style="{StaticResource DefComboBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="22"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbSettingsIPAPIUrl}" />
|
||||
<ComboBox
|
||||
x:Name="cmbIPAPIUrl"
|
||||
Grid.Row="20"
|
||||
Grid.Row="22"
|
||||
Grid.Column="1"
|
||||
Width="300"
|
||||
Margin="{StaticResource Margin8}"
|
||||
@@ -845,7 +887,7 @@
|
||||
Style="{StaticResource DefComboBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="21"
|
||||
Grid.Row="23"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -853,7 +895,7 @@
|
||||
Text="{x:Static resx:ResUI.TbSettingsSubConvert}" />
|
||||
<ComboBox
|
||||
x:Name="cmbSubConvertUrl"
|
||||
Grid.Row="21"
|
||||
Grid.Row="23"
|
||||
Grid.Column="1"
|
||||
Width="300"
|
||||
Margin="{StaticResource Margin8}"
|
||||
@@ -862,7 +904,7 @@
|
||||
Style="{StaticResource DefComboBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="22"
|
||||
Grid.Row="24"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -870,14 +912,14 @@
|
||||
Text="{x:Static resx:ResUI.TbSettingsMainGirdOrientation}" />
|
||||
<ComboBox
|
||||
x:Name="cmbMainGirdOrientation"
|
||||
Grid.Row="22"
|
||||
Grid.Row="24"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin8}"
|
||||
Style="{StaticResource DefComboBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="23"
|
||||
Grid.Row="25"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -885,14 +927,14 @@
|
||||
Text="{x:Static resx:ResUI.TbSettingsGeoFilesSource}" />
|
||||
<ComboBox
|
||||
x:Name="cmbGetFilesSourceUrl"
|
||||
Grid.Row="23"
|
||||
Grid.Row="25"
|
||||
Grid.Column="1"
|
||||
Width="300"
|
||||
Margin="{StaticResource Margin8}"
|
||||
IsEditable="True"
|
||||
Style="{StaticResource DefComboBox}" />
|
||||
<TextBlock
|
||||
Grid.Row="23"
|
||||
Grid.Row="25"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -901,7 +943,7 @@
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="24"
|
||||
Grid.Row="26"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -909,14 +951,14 @@
|
||||
Text="{x:Static resx:ResUI.TbSettingsSrsFilesSource}" />
|
||||
<ComboBox
|
||||
x:Name="cmbSrsFilesSourceUrl"
|
||||
Grid.Row="24"
|
||||
Grid.Row="26"
|
||||
Grid.Column="1"
|
||||
Width="300"
|
||||
Margin="{StaticResource Margin8}"
|
||||
IsEditable="True"
|
||||
Style="{StaticResource DefComboBox}" />
|
||||
<TextBlock
|
||||
Grid.Row="24"
|
||||
Grid.Row="26"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -925,7 +967,7 @@
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="25"
|
||||
Grid.Row="27"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -933,14 +975,14 @@
|
||||
Text="{x:Static resx:ResUI.TbSettingsRoutingRulesSource}" />
|
||||
<ComboBox
|
||||
x:Name="cmbRoutingRulesSourceUrl"
|
||||
Grid.Row="25"
|
||||
Grid.Row="27"
|
||||
Grid.Column="1"
|
||||
Width="300"
|
||||
Margin="{StaticResource Margin8}"
|
||||
IsEditable="True"
|
||||
Style="{StaticResource DefComboBox}" />
|
||||
<TextBlock
|
||||
Grid.Row="25"
|
||||
Grid.Row="27"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -1091,7 +1133,7 @@
|
||||
Margin="{StaticResource Margin8}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbSettingsTunMtu}" />
|
||||
Text="{x:Static resx:ResUI.TbMtu}" />
|
||||
<ComboBox
|
||||
x:Name="cmbMtu"
|
||||
Grid.Row="5"
|
||||
|
||||
@@ -46,6 +46,7 @@ public partial class OptionSettingWindow
|
||||
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;
|
||||
@@ -74,6 +75,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);
|
||||
@@ -102,6 +104,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);
|
||||
|
||||
@@ -158,6 +158,10 @@
|
||||
Height="{StaticResource MenuItemHeight}"
|
||||
Header="{x:Static resx:ResUI.menuRealPingServer}"
|
||||
InputGestureText="Ctrl+R" />
|
||||
<MenuItem
|
||||
x:Name="menuUdpTestServer"
|
||||
Height="{StaticResource MenuItemHeight}"
|
||||
Header="{x:Static resx:ResUI.menuUdpTestServer}" />
|
||||
<MenuItem
|
||||
x:Name="menuSpeedServer"
|
||||
Height="{StaticResource MenuItemHeight}"
|
||||
@@ -238,6 +242,10 @@
|
||||
x:Name="menuExport2ShareUrlBase64"
|
||||
Height="{StaticResource MenuItemHeight}"
|
||||
Header="{x:Static resx:ResUI.menuExport2ShareUrlBase64}" />
|
||||
<MenuItem
|
||||
x:Name="menuExport2InnerUri"
|
||||
Height="{StaticResource MenuItemHeight}"
|
||||
Header="{x:Static resx:ResUI.menuExport2InnerUri}" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="{x:Static resx:ResUI.menuGenGroupServer}">
|
||||
|
||||
@@ -71,6 +71,7 @@ public partial class ProfilesView
|
||||
this.BindCommand(ViewModel, vm => vm.MixedTestServerCmd, v => v.menuMixedTestServer).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.TcpingServerCmd, v => v.menuTcpingServer).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.RealPingServerCmd, v => v.menuRealPingServer).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.UdpTestServerCmd, v => v.menuUdpTestServer).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.SpeedServerCmd, v => v.menuSpeedServer).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.SortServerResultCmd, v => v.menuSortServerResult).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.RemoveInvalidServerResultCmd, v => v.menuRemoveInvalidServerResult).DisposeWith(disposables);
|
||||
@@ -81,6 +82,7 @@ public partial class ProfilesView
|
||||
this.BindCommand(ViewModel, vm => vm.Export2ClientConfigClipboardCmd, v => v.menuExport2ClientConfigClipboard).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlCmd, v => v.menuExport2ShareUrl).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.Export2InnerUriCmd, v => v.menuExport2InnerUri).DisposeWith(disposables);
|
||||
|
||||
AppEvents.AppExitRequested
|
||||
.AsObservable()
|
||||
|
||||
Reference in New Issue
Block a user