mirror of
https://github.com/2dust/v2rayN.git
synced 2026-09-26 00:22:07 +03:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 521230c40d | |||
| 426d5d5a21 | |||
| 3f14c86a04 | |||
| 70b8ecd3c8 | |||
| 0d452328a2 | |||
| 2a65e5ac80 | |||
| f332caf507 | |||
| 4b412e246f | |||
| 09ac4d181a | |||
| c575527725 | |||
| 589ccc6e06 | |||
| 0a2f3e4f22 | |||
| 007adb6403 | |||
| e50c0a9170 |
@@ -1,12 +1,27 @@
|
||||
name: Code Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'v2rayN/ServiceLib/**'
|
||||
- 'v2rayN/ServiceLib.UdpTest/**'
|
||||
- 'v2rayN/ServiceLib.Tests/**'
|
||||
- 'v2rayN/Directory.Build.*'
|
||||
- 'v2rayN/Directory.Packages.props'
|
||||
- 'global.json'
|
||||
- '.github/workflows/test.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'v2rayN/ServiceLib/Services/CoreConfig/**'
|
||||
- 'v2rayN/ServiceLib/Handler/Fmt/**'
|
||||
- 'v2rayN/ServiceLib/**'
|
||||
- 'v2rayN/ServiceLib.UdpTest/**'
|
||||
- 'v2rayN/ServiceLib.Tests/**'
|
||||
- 'v2rayN/Directory.Build.*'
|
||||
- 'v2rayN/Directory.Packages.props'
|
||||
- 'global.json'
|
||||
- '.github/workflows/test.yml'
|
||||
|
||||
permissions:
|
||||
@@ -19,9 +34,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: '0'
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v6
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
"test": {
|
||||
"runner": "Microsoft.Testing.Platform"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>7.24.8</Version>
|
||||
<Version>7.24.9</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
|
||||
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
|
||||
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
||||
<PackageVersion Include="TUnit" Version="1.65.38" />
|
||||
<PackageVersion Include="TUnit" Version="1.65.68" />
|
||||
<PackageVersion Include="TUnit.Assertions.Should" Version="1.65.38-beta" />
|
||||
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
||||
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
|
||||
|
||||
@@ -2,6 +2,46 @@ namespace ServiceLib.Tests.Fmt;
|
||||
|
||||
public class FmtHandlerTests
|
||||
{
|
||||
/// <summary>
|
||||
/// One profile factory per protocol that <see cref="FmtHandler.GetShareUri" /> can export.
|
||||
/// The suite below asserts that this map and <see cref="Global.ProtocolShares" /> agree, so a
|
||||
/// newly exportable protocol cannot be added without a round-trip case.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<EConfigType, Func<ProfileItem>> ShareProfileFactories = new()
|
||||
{
|
||||
[EConfigType.VMess] = CreateVmessProfile,
|
||||
[EConfigType.Shadowsocks] = CreateShadowsocksProfile,
|
||||
[EConfigType.SOCKS] = CreateSocksProfile,
|
||||
[EConfigType.VLESS] = CreateVlessProfile,
|
||||
[EConfigType.Trojan] = CreateTrojanProfile,
|
||||
[EConfigType.Hysteria2] = CreateHysteria2Profile,
|
||||
[EConfigType.TUIC] = CreateTuicProfile,
|
||||
[EConfigType.WireGuard] = CreateWireguardProfile,
|
||||
[EConfigType.Anytls] = CreateAnytlsProfile,
|
||||
[EConfigType.Naive] = () => CreateNaiveProfile(false),
|
||||
};
|
||||
|
||||
[Test]
|
||||
public async Task ShareUriSuite_ShouldCoverAndRoundTripEveryExportableProtocol()
|
||||
{
|
||||
var uncovered = string.Join(", ", Global.ProtocolShares.Keys.Except(ShareProfileFactories.Keys));
|
||||
var unexpected = string.Join(", ", ShareProfileFactories.Keys.Except(Global.ProtocolShares.Keys));
|
||||
|
||||
await uncovered.Should().BeEqualTo(string.Empty);
|
||||
await unexpected.Should().BeEqualTo(string.Empty);
|
||||
|
||||
foreach (var (configType, factory) in ShareProfileFactories)
|
||||
{
|
||||
var source = factory();
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
await resolved.ConfigType.Should().BeEqualTo(configType);
|
||||
await resolved.Address.Should().BeEqualTo(source.Address);
|
||||
await resolved.Port.Should().BeEqualTo(source.Port);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Vmess_ShouldRoundTripBasicFields()
|
||||
{
|
||||
@@ -62,6 +102,165 @@ public class FmtHandlerTests
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Trojan_ShouldRoundTripBasicFields()
|
||||
{
|
||||
var source = CreateTrojanProfile();
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await AssertRawTransportFields(source, resolved);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.Sni.Should().BeEqualTo(source.Sni);
|
||||
await resolved.GetProtocolExtra().Flow.Should().BeEqualTo(source.GetProtocolExtra().Flow);
|
||||
await resolved.GetAllowInsecure().Should().BeTrue();
|
||||
|
||||
// Trojan is the one exporter that writes both spellings of the flag.
|
||||
await AssertExportContains(source, "allowInsecure=1", "insecure=1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Tuic_ShouldRoundTripUserInfoAndCongestionControl()
|
||||
{
|
||||
var source = CreateTuicProfile();
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await resolved.Username.Should().BeEqualTo(source.Username);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.Sni.Should().BeEqualTo(source.Sni);
|
||||
await resolved.Alpn.Should().BeEqualTo(source.Alpn);
|
||||
await resolved.GetProtocolExtra().CongestionControl.Should()
|
||||
.BeEqualTo(source.GetProtocolExtra().CongestionControl);
|
||||
await resolved.GetAllowInsecure().Should().BeTrue();
|
||||
|
||||
await AssertExportContains(source, "allow_insecure=1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Anytls_ShouldRoundTripBasicFields()
|
||||
{
|
||||
var source = CreateAnytlsProfile();
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await AssertRawTransportFields(source, resolved);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.Sni.Should().BeEqualTo(source.Sni);
|
||||
await resolved.Alpn.Should().BeEqualTo(source.Alpn);
|
||||
await resolved.GetAllowInsecure().Should().BeTrue();
|
||||
|
||||
await AssertExportContains(source, "insecure=1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Hysteria2_ShouldRoundTripObfsAndNormalizePortRange()
|
||||
{
|
||||
var source = CreateHysteria2Profile();
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
var sourceExtra = source.GetProtocolExtra();
|
||||
var resolvedExtra = resolved.GetProtocolExtra();
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.Sni.Should().BeEqualTo(source.Sni);
|
||||
await resolved.Alpn.Should().BeEqualTo(source.Alpn);
|
||||
await resolved.EchConfigList.Should().BeEqualTo(source.EchConfigList);
|
||||
await resolved.GetAllowInsecure().Should().BeTrue();
|
||||
await resolvedExtra.SalamanderPass.Should().BeEqualTo(sourceExtra.SalamanderPass);
|
||||
|
||||
// Hysteria2Fmt stores a port range internally as "5000:6000" and emits the URI form.
|
||||
await resolvedExtra.Ports.Should().BeEqualTo("5000-6000");
|
||||
|
||||
await AssertExportContains(source, "insecure=1", "obfs=salamander", "mport=5000-6000");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Wireguard_ShouldRoundTripKeysAndInterface()
|
||||
{
|
||||
var source = CreateWireguardProfile();
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
var extra = resolved.GetProtocolExtra();
|
||||
var sourceExtra = source.GetProtocolExtra();
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await extra.WgPublicKey.Should().BeEqualTo(sourceExtra.WgPublicKey);
|
||||
await extra.WgPresharedKey.Should().BeEqualTo(sourceExtra.WgPresharedKey);
|
||||
await extra.WgReserved.Should().BeEqualTo(sourceExtra.WgReserved);
|
||||
await extra.WgInterfaceAddress.Should().BeEqualTo(sourceExtra.WgInterfaceAddress);
|
||||
await extra.WgMtu.Should().BeEqualTo(sourceExtra.WgMtu);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUri_Wireguard_ShouldEncodeKeysAndBracketIpv6()
|
||||
{
|
||||
var source = CreateWireguardProfile();
|
||||
|
||||
// Base64 keys carry '/', '+' and '=', and the address is an IPv6 literal: both have to
|
||||
// survive the wire form, which a round trip through the same encoder would not prove.
|
||||
await AssertExportContains(
|
||||
source,
|
||||
Uri.EscapeDataString(source.Password),
|
||||
Uri.EscapeDataString(source.GetProtocolExtra().WgPublicKey ?? string.Empty),
|
||||
"@[2001:db8::40]:51820");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_Naive_ShouldRoundTripCredentialsOverHttps()
|
||||
{
|
||||
var source = CreateNaiveProfile(false);
|
||||
|
||||
var resolved = await ExportThenImport(source, Global.NaiveHttpsProtocolShare);
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await AssertRawTransportFields(source, resolved);
|
||||
await resolved.Username.Should().BeEqualTo(source.Username);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.GetProtocolExtra().InsecureConcurrency.Should()
|
||||
.BeEqualTo(source.GetProtocolExtra().InsecureConcurrency);
|
||||
|
||||
// NaiveFmt only ever sets this flag on the quic branch, so the https branch leaves it
|
||||
// unset rather than false - assert "is not quic" instead of an explicit false.
|
||||
await (resolved.GetProtocolExtra().NaiveQuic == true).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetShareUriAndResolveConfig_NaiveQuic_ShouldRoundTripQuicScheme()
|
||||
{
|
||||
var source = CreateNaiveProfile(true);
|
||||
|
||||
var resolved = await ExportThenImport(source, Global.NaiveQuicProtocolShare);
|
||||
|
||||
await AssertCommonShareFields(source, resolved);
|
||||
await AssertRawTransportFields(source, resolved);
|
||||
await resolved.Username.Should().BeEqualTo(source.Username);
|
||||
await resolved.Password.Should().BeEqualTo(source.Password);
|
||||
await resolved.GetProtocolExtra().InsecureConcurrency.Should()
|
||||
.BeEqualTo(source.GetProtocolExtra().InsecureConcurrency);
|
||||
await resolved.GetProtocolExtra().NaiveQuic.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("p:a@ss#% +/=")]
|
||||
[Arguments("пароль 東京")]
|
||||
public async Task GetShareUriAndResolveConfig_Trojan_ShouldRoundTripEncodedCredentials(string password)
|
||||
{
|
||||
var source = CreateTrojanProfile();
|
||||
source.Password = password;
|
||||
source.Remarks = "Trojan — тест 東京 #1";
|
||||
|
||||
var resolved = await ExportThenImport(source);
|
||||
|
||||
await resolved.Password.Should().BeEqualTo(password);
|
||||
await resolved.Remarks.Should().BeEqualTo(source.Remarks);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResolveConfig_UnsupportedProtocol_ShouldReturnNull()
|
||||
{
|
||||
@@ -82,14 +281,70 @@ public class FmtHandlerTests
|
||||
await uri.Should().BeNull();
|
||||
}
|
||||
|
||||
private static async Task AssertCommonShareFields(ProfileItem source, ProfileItem resolved)
|
||||
{
|
||||
await resolved.ConfigType.Should().BeEqualTo(source.ConfigType);
|
||||
await resolved.Remarks.Should().BeEqualTo(source.Remarks);
|
||||
await resolved.Address.Should().BeEqualTo(source.Address);
|
||||
await resolved.Port.Should().BeEqualTo(source.Port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only for protocols whose exporter goes through the shared transport query
|
||||
/// (<c>security</c>, <c>type</c>, <c>headerType</c>). TUIC, Hysteria2 and WireGuard do not.
|
||||
/// </summary>
|
||||
private static async Task AssertRawTransportFields(ProfileItem source, ProfileItem resolved)
|
||||
{
|
||||
await resolved.Network.Should().BeEqualTo(source.Network);
|
||||
await resolved.StreamSecurity.Should().BeEqualTo(source.StreamSecurity);
|
||||
await resolved.GetTransportExtra().RawHeaderType.Should()
|
||||
.BeEqualTo(source.GetTransportExtra().RawHeaderType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts on the wire form itself. A round trip cannot catch an exporter and an importer that
|
||||
/// agree on the wrong spelling of a parameter, and the insecure flag is spelled differently by
|
||||
/// every protocol.
|
||||
/// </summary>
|
||||
private static async Task AssertExportContains(ProfileItem source, params string[] expectedFragments)
|
||||
{
|
||||
var uri = FmtHandler.GetShareUri(source);
|
||||
|
||||
await uri.Should().NotBeNull();
|
||||
|
||||
foreach (var fragment in expectedFragments)
|
||||
{
|
||||
await uri!.Contains(fragment, StringComparison.Ordinal).Should()
|
||||
.BeTrue().Because($"uri: {uri}, expected fragment: {fragment}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExpectedShareScheme(ProfileItem item)
|
||||
{
|
||||
if (item.ConfigType != EConfigType.Naive)
|
||||
{
|
||||
return Global.ProtocolShares[item.ConfigType];
|
||||
}
|
||||
|
||||
// NaiveFmt never emits the "naive://" prefix that Global.ProtocolShares records for the
|
||||
// type; that entry is only read when importing.
|
||||
return item.GetProtocolExtra().NaiveQuic == true
|
||||
? Global.NaiveQuicProtocolShare
|
||||
: Global.NaiveHttpsProtocolShare;
|
||||
}
|
||||
|
||||
private static async Task<ProfileItem> ExportThenImport(ProfileItem source)
|
||||
{
|
||||
return await ExportThenImport(source, ExpectedShareScheme(source));
|
||||
}
|
||||
|
||||
private static async Task<ProfileItem> ExportThenImport(ProfileItem source, string expectedPrefix)
|
||||
{
|
||||
var uri = FmtHandler.GetShareUri(source);
|
||||
|
||||
await uri.Should().NotBeNull();
|
||||
await uri.Should().NotBeEmpty();
|
||||
await uri!.StartsWith(Global.ProtocolShares[source.ConfigType], StringComparison.OrdinalIgnoreCase).Should()
|
||||
.BeTrue();
|
||||
await uri!.StartsWith(expectedPrefix, StringComparison.OrdinalIgnoreCase).Should().BeTrue();
|
||||
|
||||
var resolved = FmtHandler.ResolveConfig(uri, out var msg);
|
||||
|
||||
@@ -166,4 +421,142 @@ public class FmtHandlerTests
|
||||
Password = "pass",
|
||||
};
|
||||
}
|
||||
|
||||
private static ProfileItem CreateTrojanProfile()
|
||||
{
|
||||
var item = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.Trojan,
|
||||
Remarks = "trojan demo",
|
||||
Address = "trojan.example",
|
||||
Port = 443,
|
||||
Password = "trojan-pass",
|
||||
Network = nameof(ETransport.raw),
|
||||
StreamSecurity = Global.StreamSecurity,
|
||||
Sni = "sni.trojan.example",
|
||||
AllowInsecure = Global.StringTrue,
|
||||
};
|
||||
|
||||
item.SetProtocolExtra(new ProtocolExtraItem { Flow = Global.Flows[1], });
|
||||
item.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None, });
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private static ProfileItem CreateTuicProfile()
|
||||
{
|
||||
var item = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.TUIC,
|
||||
Remarks = "tuic demo",
|
||||
Address = "tuic.example",
|
||||
Port = 8443,
|
||||
// A colon separates the two halves of the TUIC user info, so it cannot appear in the
|
||||
// uuid; a fixed value also keeps a failure reproducible.
|
||||
Username = "01234567-89ab-cdef-0123-456789abcdef",
|
||||
Password = "tuic-pass",
|
||||
Sni = "sni.tuic.example",
|
||||
Alpn = "h3",
|
||||
AllowInsecure = Global.StringTrue,
|
||||
};
|
||||
|
||||
item.SetProtocolExtra(new ProtocolExtraItem { CongestionControl = "bbr", });
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private static ProfileItem CreateAnytlsProfile()
|
||||
{
|
||||
var item = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.Anytls,
|
||||
Remarks = "anytls demo",
|
||||
Address = "anytls.example",
|
||||
Port = 8443,
|
||||
Password = "anytls-pass",
|
||||
Network = nameof(ETransport.raw),
|
||||
StreamSecurity = Global.StreamSecurity,
|
||||
Sni = "sni.anytls.example",
|
||||
Alpn = "h2,http/1.1",
|
||||
AllowInsecure = Global.StringTrue,
|
||||
};
|
||||
|
||||
item.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None, });
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private static ProfileItem CreateHysteria2Profile()
|
||||
{
|
||||
// CertSha is deliberately left unset: the importer turns AllowInsecure on by itself when a
|
||||
// pinSHA256 is present, which would mask an exporter that stopped emitting insecure=1.
|
||||
var item = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.Hysteria2,
|
||||
Remarks = "hysteria2 demo",
|
||||
Address = "hy2.example",
|
||||
Port = 8443,
|
||||
Password = "demo-user:demo-pass",
|
||||
Sni = "sni.hy2.example",
|
||||
Alpn = "h3",
|
||||
EchConfigList = "AAj+DQAEAAAAAA==",
|
||||
AllowInsecure = Global.StringTrue,
|
||||
};
|
||||
|
||||
item.SetProtocolExtra(new ProtocolExtraItem
|
||||
{
|
||||
SalamanderPass = "salamander-pass",
|
||||
Ports = "5000:6000",
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private static string CreateWireguardKey(byte value)
|
||||
{
|
||||
return Convert.ToBase64String(Enumerable.Repeat(value, 32).ToArray());
|
||||
}
|
||||
|
||||
private static ProfileItem CreateWireguardProfile()
|
||||
{
|
||||
var item = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.WireGuard,
|
||||
Remarks = "WireGuard — тест 東京 #1",
|
||||
Address = "2001:db8::40",
|
||||
Port = 51820,
|
||||
Password = CreateWireguardKey(0xFE),
|
||||
};
|
||||
|
||||
item.SetProtocolExtra(new ProtocolExtraItem
|
||||
{
|
||||
WgPublicKey = CreateWireguardKey(0xFD),
|
||||
WgPresharedKey = CreateWireguardKey(0xFC),
|
||||
WgReserved = "1,2,255",
|
||||
WgInterfaceAddress = "10.0.0.2/32,fd00::2/128",
|
||||
WgMtu = 1420,
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private static ProfileItem CreateNaiveProfile(bool quic)
|
||||
{
|
||||
var item = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.Naive,
|
||||
Remarks = quic ? "naive quic demo" : "naive https demo",
|
||||
Address = "naive.example",
|
||||
Port = 443,
|
||||
Username = "naive-user",
|
||||
Password = "päss:word@/?#&=+ 東京",
|
||||
Network = nameof(ETransport.raw),
|
||||
StreamSecurity = Global.None,
|
||||
};
|
||||
|
||||
item.SetProtocolExtra(new ProtocolExtraItem { NaiveQuic = quic, InsecureConcurrency = 4, });
|
||||
item.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None, });
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace ServiceLib.Tests.Fmt;
|
||||
|
||||
public class Hysteria2FmtTests
|
||||
{
|
||||
// "The hostname and optional port of the server. If the port is omitted, it defaults to 443."
|
||||
// -- https://v2.hysteria.network/docs/developers/URI-Scheme/
|
||||
// A ':' with no digits after it is an omitted port too, per RFC 3986 'port = *DIGIT'.
|
||||
[Test]
|
||||
[Arguments("hysteria2://password@hy2.example/")]
|
||||
[Arguments("hysteria2://password@hy2.example")]
|
||||
[Arguments("hysteria2://password@hy2.example:/")]
|
||||
[Arguments("hy2://password@hy2.example/?sni=real.example")]
|
||||
public async Task ResolveConfig_WithoutPort_ShouldDefaultTo443(string shareUri)
|
||||
{
|
||||
var resolved = FmtHandler.ResolveConfig(shareUri, out var msg);
|
||||
|
||||
await resolved.Should().NotBeNull().Because($"uri: {shareUri}, msg: {msg}");
|
||||
await resolved!.ConfigType.Should().BeEqualTo(EConfigType.Hysteria2);
|
||||
await resolved.Address.Should().BeEqualTo("hy2.example");
|
||||
await resolved.Port.Should().BeEqualTo(443);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResolveConfig_WithoutPort_ShouldProduceAValidProfile()
|
||||
{
|
||||
// Uri.Port is -1 for an unregistered scheme with no port, and ProfileItem.IsValid rejects
|
||||
// any port outside 1..65535 - so the default is what keeps such a link usable at all.
|
||||
var resolved = FmtHandler.ResolveConfig("hysteria2://password@hy2.example/", out _);
|
||||
|
||||
await resolved.Should().NotBeNull();
|
||||
await resolved!.IsValid().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResolveConfig_WithExplicitPort_ShouldKeepIt()
|
||||
{
|
||||
var resolved = FmtHandler.ResolveConfig("hysteria2://password@hy2.example:8443/", out _);
|
||||
|
||||
await resolved.Should().NotBeNull();
|
||||
await resolved!.Port.Should().BeEqualTo(8443);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResolveConfig_WithExplicitZeroPort_ShouldNotApplyTheDefault()
|
||||
{
|
||||
// Uri.Port is 0 here, not -1: the port is present, it is just not a usable one. Treating
|
||||
// it as "omitted" would silently move the endpoint to :443, so it stays invalid instead.
|
||||
var resolved = FmtHandler.ResolveConfig("hysteria2://password@hy2.example:0/", out _);
|
||||
|
||||
await resolved.Should().NotBeNull();
|
||||
await resolved!.Port.Should().BeEqualTo(0);
|
||||
await resolved.IsValid().Should().BeFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
namespace ServiceLib.Tests.Fmt;
|
||||
|
||||
public class ShareUriQueryTests
|
||||
{
|
||||
[Test]
|
||||
[Arguments("ob%41fs")]
|
||||
[Arguments("66%ff")]
|
||||
[Arguments("100%")]
|
||||
public async Task GetShareUriAndResolveConfig_QueryValueWithPercent_ShouldSurviveTheRoundTrip(string obfsPassword)
|
||||
{
|
||||
var source = new ProfileItem
|
||||
{
|
||||
ConfigType = EConfigType.Hysteria2,
|
||||
Remarks = "percent demo",
|
||||
Address = "hy2.example",
|
||||
Port = 8443,
|
||||
Password = "pw",
|
||||
};
|
||||
source.SetProtocolExtra(new ProtocolExtraItem { SalamanderPass = obfsPassword, });
|
||||
|
||||
var uri = FmtHandler.GetShareUri(source);
|
||||
|
||||
await uri.Should().NotBeNull();
|
||||
|
||||
var resolved = FmtHandler.ResolveConfig(uri!, out var msg);
|
||||
|
||||
await resolved.Should().NotBeNull().Because($"uri: {uri}, msg: {msg}");
|
||||
await resolved!.GetProtocolExtra().SalamanderPass.Should().BeEqualTo(obfsPassword);
|
||||
}
|
||||
|
||||
// RFC 3986 lists '=' among the sub-delimiters a query value may carry, so only the first one
|
||||
// separates the key from the value. Splitting on every '=' discarded the pair outright.
|
||||
[Test]
|
||||
public async Task ResolveConfig_QueryValueWithUnescapedEquals_ShouldNotBeDropped()
|
||||
{
|
||||
const string shareUri = "hysteria2://pw@hy2.example:8443/?ech=AAj+DQAEAAAAAA==&sni=real.example";
|
||||
|
||||
var resolved = FmtHandler.ResolveConfig(shareUri, out var msg);
|
||||
|
||||
await resolved.Should().NotBeNull().Because($"uri: {shareUri}, msg: {msg}");
|
||||
await resolved!.EchConfigList.Should().BeEqualTo("AAj+DQAEAAAAAA==");
|
||||
await resolved.Sni.Should().BeEqualTo("real.example");
|
||||
}
|
||||
|
||||
// Canonical SIP002 percent-encodes the plugin argument, and that is what this client's own
|
||||
// exporter emits. This covers the non-canonical spelling instead: the options are a ';'
|
||||
// separated list of 'key=value' pairs, so left unescaped the value always carries '='.
|
||||
[Test]
|
||||
public async Task ResolveConfig_NonCanonicalSip002PluginWithLiteralEquals_ShouldConfigureObfs()
|
||||
{
|
||||
const string shareUri =
|
||||
"ss://YWVzLTEyOC1nY206cGFzczEyMw==@1.2.3.4:8388/?plugin=obfs-local;obfs=http;obfs-host=example.com#ss";
|
||||
|
||||
var resolved = FmtHandler.ResolveConfig(shareUri, out var msg);
|
||||
|
||||
await resolved.Should().NotBeNull().Because($"uri: {shareUri}, msg: {msg}");
|
||||
await resolved!.ConfigType.Should().BeEqualTo(EConfigType.Shadowsocks);
|
||||
await resolved.GetTransportExtra().Host.Should().BeEqualTo("example.com");
|
||||
await resolved.GetTransportExtra().RawHeaderType.Should().BeEqualTo(Global.RawHeaderHttp);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ResolveConfig_EscapedQueryValue_ShouldStillDecodeExactlyOnce()
|
||||
{
|
||||
const string shareUri = "hysteria2://pw@hy2.example:8443/?ech=AAj%2BDQAEAAAAAA%3D%3D&obfs=salamander&obfs-password=a%20b";
|
||||
|
||||
var resolved = FmtHandler.ResolveConfig(shareUri, out var msg);
|
||||
|
||||
await resolved.Should().NotBeNull().Because($"uri: {shareUri}, msg: {msg}");
|
||||
await resolved!.EchConfigList.Should().BeEqualTo("AAj+DQAEAAAAAA==");
|
||||
await resolved.GetProtocolExtra().SalamanderPass.Should().BeEqualTo("a b");
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ public class WireguardFmtTests
|
||||
PrivateKey = interface-private-key
|
||||
Address = 10.0.0.2/32, fd00::2/128 ; inline comment
|
||||
MTU = 1420
|
||||
DNS = 2001::db8::53, 1.2.3.4, 2001:db8::54
|
||||
|
||||
[Peer]
|
||||
PublicKey = peer-public-key
|
||||
@@ -35,6 +36,7 @@ public class WireguardFmtTests
|
||||
await first.GetProtocolExtra().WgReserved.Should().BeEqualTo("1, 2, 3");
|
||||
await first.GetProtocolExtra().WgInterfaceAddress.Should().BeEqualTo("10.0.0.2/32, fd00::2/128");
|
||||
await first.GetProtocolExtra().WgMtu.Should().BeEqualTo(1420);
|
||||
await first.GetProtocolExtra().WgDns.Should().BeEqualTo("2001::db8::53, 1.2.3.4, 2001:db8::54");
|
||||
|
||||
var second = resolved[1];
|
||||
await second.Address.Should().BeEqualTo("example.com");
|
||||
|
||||
@@ -200,7 +200,9 @@ public class Utils
|
||||
var parts = query[1..].Split('&', StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var keyValue = part.Split('=');
|
||||
// Split on the FIRST '=' only: RFC 3986 lists '=' among the sub-delimiters a query
|
||||
// value may carry, so everything after the first one belongs to the value.
|
||||
var keyValue = part.Split('=', 2);
|
||||
if (keyValue.Length != 2)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -922,6 +922,7 @@ public static class ConfigHandler
|
||||
WgInterfaceAddress = profileItem.GetProtocolExtra().WgInterfaceAddress?.TrimEx(),
|
||||
WgReserved = wgReserved,
|
||||
WgMtu = profileItem.GetProtocolExtra().WgMtu is null or <= 0 ? Global.TunMtus.First() : profileItem.GetProtocolExtra().WgMtu,
|
||||
WgDns = profileItem.GetProtocolExtra().WgDns?.TrimEx(),
|
||||
});
|
||||
|
||||
if (profileItem.Password.IsNullOrEmpty())
|
||||
@@ -1753,15 +1754,13 @@ public static class ConfigHandler
|
||||
{
|
||||
lstProfiles = SingboxFmt.ResolveToCustomOutbound(strData, subRemarks);
|
||||
}
|
||||
if (lstProfiles.Count == 0)
|
||||
if (lstProfiles.Count > 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
|
||||
if (count > 0)
|
||||
{
|
||||
return count;
|
||||
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
|
||||
if (count > 0)
|
||||
{
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
if (HtmlPageFmt.IsHtmlPage(strData))
|
||||
@@ -1801,15 +1800,13 @@ public static class ConfigHandler
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if ((lstProfiles?.Count ?? 0) == 0)
|
||||
if (lstProfiles?.Count > 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
|
||||
if (count > 0)
|
||||
{
|
||||
return count;
|
||||
var count = await AddBatchCustomServers(config, lstProfiles, subid, isSub);
|
||||
if (count > 0)
|
||||
{
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
return await SaveCustomRawFileServer(config, strData, subid, isSub, subItem, customCoreType);
|
||||
@@ -2614,7 +2611,7 @@ public static class ConfigHandler
|
||||
items = await AppManager.Instance.RoutingItems();
|
||||
}
|
||||
|
||||
if (!blImportAdvancedRules && items.Count(u => u.Remarks.StartsWith(ver)) > 0)
|
||||
if (!blImportAdvancedRules && items.Count() > 0) // items.Count(u => u.Remarks.StartsWith(ver)) > 0)
|
||||
{
|
||||
//migrate
|
||||
//TODO Temporary code to be removed later
|
||||
|
||||
@@ -342,8 +342,13 @@ public class BaseFmt
|
||||
return query[key] ?? defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Values are already unescaped by <see cref="Utils.ParseQueryString" />, so this must not
|
||||
/// unescape them a second time: a value that still holds a valid percent sequence after the
|
||||
/// first pass - an obfuscation password of "ob%41fs", say - would decay into "obAfs".
|
||||
/// </summary>
|
||||
protected static string GetQueryDecoded(NameValueCollection query, string key, string defaultValue = "")
|
||||
{
|
||||
return Utils.UrlDecode(GetQueryValue(query, key, defaultValue));
|
||||
return GetQueryValue(query, key, defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@ public class Hysteria2Fmt : BaseFmt
|
||||
}
|
||||
|
||||
item.Address = url.IdnHost;
|
||||
item.Port = url.Port;
|
||||
// The URI scheme makes the port optional and defaults it to 443. Uri.Port answers -1 for
|
||||
// an unregistered scheme carrying no port, which ProfileItem.IsValid then rejects.
|
||||
// Only -1 means "omitted": an explicit ":0" has to stay 0 and be rejected the way it
|
||||
// always was, instead of being quietly redirected to a server the link never named.
|
||||
item.Port = url.Port == -1 ? 443 : url.Port;
|
||||
item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped);
|
||||
item.Password = Utils.UrlDecode(url.UserInfo);
|
||||
|
||||
@@ -223,6 +227,11 @@ public class Hysteria2Fmt : BaseFmt
|
||||
var sha = item.CertSha;
|
||||
dicQuery.Add("pinSHA256", Utils.UrlEncode(sha));
|
||||
}
|
||||
else if (!item.Cert.IsNullOrEmpty()
|
||||
&& CertPemManager.GetLeafCertSha256Thumbprint(item.Cert) is { Length: > 0 } thumbprint)
|
||||
{
|
||||
dicQuery.Add("pinSHA256", Utils.UrlEncode(thumbprint));
|
||||
}
|
||||
if (!item.EchConfigList.IsNullOrEmpty())
|
||||
{
|
||||
dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList));
|
||||
|
||||
@@ -31,6 +31,7 @@ public class WireguardFmt : BaseFmt
|
||||
WgReserved = GetQueryDecoded(query, "reserved"),
|
||||
WgInterfaceAddress = GetQueryDecoded(query, "address"),
|
||||
WgMtu = int.TryParse(GetQueryDecoded(query, "mtu"), out var mtuVal) ? mtuVal : null,
|
||||
WgDns = GetQueryDecoded(query, "dns"),
|
||||
});
|
||||
|
||||
return item;
|
||||
@@ -71,6 +72,10 @@ public class WireguardFmt : BaseFmt
|
||||
{
|
||||
dicQuery.Add("mtu", protoExtra.WgMtu.ToString());
|
||||
}
|
||||
if (!protoExtra.WgDns.IsNullOrEmpty())
|
||||
{
|
||||
dicQuery.Add("dns", Utils.UrlEncode(protoExtra.WgDns));
|
||||
}
|
||||
return ToUri(EConfigType.WireGuard, item.Address, item.Port, item.Password, dicQuery, remark);
|
||||
}
|
||||
|
||||
@@ -133,6 +138,7 @@ public class WireguardFmt : BaseFmt
|
||||
|
||||
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 wgDns = interfaceDic.TryGetValue("DNS", out var dns) ? dns : string.Empty;
|
||||
|
||||
var index = 0;
|
||||
var resultList = new List<ProfileItem>();
|
||||
@@ -156,6 +162,7 @@ public class WireguardFmt : BaseFmt
|
||||
WgInterfaceAddress = wgInterfaceAddress,
|
||||
WgReserved = (peerDic.TryGetValue("Reserved", out var reserved) ? reserved : string.Empty).NullIfEmpty(),
|
||||
WgMtu = wgMtu > 0 ? wgMtu : null,
|
||||
WgDns = wgDns,
|
||||
};
|
||||
|
||||
var item = new ProfileItem
|
||||
|
||||
@@ -281,6 +281,37 @@ public class CertPemManager
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetLeafCertSha256Thumbprint(string pemCertChain, bool includeColon = false)
|
||||
{
|
||||
var certs = ParsePemChain(pemCertChain);
|
||||
if (certs.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
foreach (var certStr in certs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cert = X509Certificate2.CreateFromPem(certStr);
|
||||
var extension = cert.Extensions
|
||||
.OfType<X509BasicConstraintsExtension>()
|
||||
.FirstOrDefault();
|
||||
var isCa = extension?.CertificateAuthority == true;
|
||||
if (isCa)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256);
|
||||
return includeColon ? string.Join(":", thumbprint.Chunk(2).Select(c => new string(c))) : thumbprint;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static readonly Lazy<X509Certificate2Collection> _chromeRootCerts = new(() =>
|
||||
{
|
||||
var pemText = EmbedUtils.GetEmbedText(Global.ChromeRootCertFileName);
|
||||
|
||||
@@ -172,6 +172,8 @@ public class Outboundsettings4Ray
|
||||
public int? workers { get; set; }
|
||||
|
||||
public int? version { get; set; }
|
||||
|
||||
public List<string>? remoteDNS { get; set; }
|
||||
}
|
||||
|
||||
public class WireguardPeer4Ray
|
||||
|
||||
@@ -27,6 +27,7 @@ public record ProtocolExtraItem
|
||||
public string? WgInterfaceAddress { get; init; }
|
||||
public string? WgReserved { get; init; }
|
||||
public int? WgMtu { get; init; }
|
||||
public string? WgDns { get; init; }
|
||||
|
||||
// hysteria2
|
||||
public string? SalamanderPass { get; init; }
|
||||
|
||||
Generated
+9
@@ -3015,6 +3015,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 DNS 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbDNS {
|
||||
get {
|
||||
return ResourceManager.GetString("TbDNS", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 DNS Hosts: ("domain1 ip1 ip2" per line) 的本地化字符串。
|
||||
/// </summary>
|
||||
|
||||
@@ -1869,4 +1869,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
|
||||
<value>Block IPv6 queries when enabled</value>
|
||||
</data>
|
||||
<data name="TbDNS" xml:space="preserve">
|
||||
<value>DNS</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1462,7 +1462,7 @@
|
||||
<value>Включён пользовательский DNS — настройки на этой странице не применяются</value>
|
||||
</data>
|
||||
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
|
||||
<value>Block ECH and HTTP/3 availability checks when enabled. Always enabled in Xray</value>
|
||||
<value>При включении блокирует запросы доступности ECH и HTTP/3. В Xray включено всегда</value>
|
||||
</data>
|
||||
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
|
||||
<value>Пожалуйста, заполните корректный шаблон конфигурации</value>
|
||||
@@ -1860,4 +1860,13 @@
|
||||
<data name="LvCustomCoreType" xml:space="preserve">
|
||||
<value>Ядро пользовательской конфигурации</value>
|
||||
</data>
|
||||
<data name="TbXrayOnly" xml:space="preserve">
|
||||
<value>Только Xray</value>
|
||||
</data>
|
||||
<data name="TbBlockAAAAQueries" xml:space="preserve">
|
||||
<value>Блокировать DNS-запросы AAAA</value>
|
||||
</data>
|
||||
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
|
||||
<value>При включении блокирует DNS-запросы IPv6</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1870,4 +1870,7 @@
|
||||
<data name="TbBlockAAAAQueriesTips" xml:space="preserve">
|
||||
<value>开启后将阻止 IPv6 查询</value>
|
||||
</data>
|
||||
<data name="TbDNS" xml:space="preserve">
|
||||
<value>DNS</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -306,8 +306,8 @@ public partial class CoreConfigSingboxService
|
||||
}
|
||||
|
||||
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet) ?? [];
|
||||
var expectedIPCidr = new List<string>();
|
||||
var expectedIPsRegions = new List<string>();
|
||||
var expectedIPCidr = new HashSet<string>();
|
||||
var expectedIPsRegions = new HashSet<string>();
|
||||
var regionName = string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(simpleDnsItem?.DirectExpectedIPs))
|
||||
@@ -316,7 +316,7 @@ public partial class CoreConfigSingboxService
|
||||
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.Trim())
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.ToList();
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var ip in ipItems)
|
||||
{
|
||||
@@ -374,11 +374,11 @@ public partial class CoreConfigSingboxService
|
||||
rule4ExpectedIPs.geosite = regionGeosite;
|
||||
if (expectedIPsRegions.Count > 0)
|
||||
{
|
||||
rule4ExpectedIPs.geoip = expectedIPsRegions;
|
||||
rule4ExpectedIPs.geoip = expectedIPsRegions.ToList();
|
||||
}
|
||||
if (expectedIPCidr.Count > 0)
|
||||
{
|
||||
rule4ExpectedIPs.ip_cidr = expectedIPCidr;
|
||||
rule4ExpectedIPs.ip_cidr = expectedIPCidr.ToList();
|
||||
}
|
||||
_coreConfig.dns.rules.Add(rule4ExpectedIPs);
|
||||
}
|
||||
|
||||
@@ -159,16 +159,16 @@ public partial class CoreConfigV2rayService
|
||||
var directDNSAddress = ParseDnsAddresses(simpleDNSItem?.DirectDNS, Global.DomainDirectDNSAddress.First());
|
||||
var remoteDNSAddress = ParseDnsAddresses(simpleDNSItem?.RemoteDNS, Global.DomainRemoteDNSAddress.First());
|
||||
|
||||
var directDomainList = new List<string>();
|
||||
var directGeositeList = new List<string>();
|
||||
var proxyDomainList = new List<string>();
|
||||
var proxyGeositeList = new List<string>();
|
||||
var expectedDomainList = new List<string>();
|
||||
var expectedIPs = new List<string>();
|
||||
var directDomainList = new HashSet<string>();
|
||||
var directGeositeList = new HashSet<string>();
|
||||
var proxyDomainList = new HashSet<string>();
|
||||
var proxyGeositeList = new HashSet<string>();
|
||||
var expectedDomainList = new HashSet<string>();
|
||||
var expectedIPs = new HashSet<string>();
|
||||
var regionName = string.Empty;
|
||||
|
||||
var bootstrapDNSAddress = ParseDnsAddresses(simpleDNSItem?.BootstrapDNS, Global.DomainPureIPDNSAddress.First());
|
||||
var dnsServerDomains = new List<string>();
|
||||
var dnsServerDomains = new HashSet<string>();
|
||||
|
||||
foreach (var dns in directDNSAddress)
|
||||
{
|
||||
@@ -194,7 +194,6 @@ public partial class CoreConfigV2rayService
|
||||
dnsServerDomains.Add($"full:{domain}");
|
||||
}
|
||||
}
|
||||
dnsServerDomains = dnsServerDomains.Distinct().ToList();
|
||||
|
||||
if (!string.IsNullOrEmpty(simpleDNSItem?.DirectExpectedIPs))
|
||||
{
|
||||
@@ -202,7 +201,7 @@ public partial class CoreConfigV2rayService
|
||||
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.Trim())
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.ToList();
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var region in from ip in expectedIPs
|
||||
where ip.StartsWith(Global.GeoIPPrefix, StringComparison.OrdinalIgnoreCase)
|
||||
@@ -269,15 +268,19 @@ public partial class CoreConfigV2rayService
|
||||
}
|
||||
}
|
||||
|
||||
if (context.ProtectDomainList.Count > 0)
|
||||
{
|
||||
directDomainList.AddRange(context.ProtectDomainList);
|
||||
}
|
||||
|
||||
dnsItem.servers ??= [];
|
||||
|
||||
var directDnsTagIndex = 1;
|
||||
|
||||
if (dnsServerDomains.Count > 0)
|
||||
{
|
||||
AddDnsServers(bootstrapDNSAddress, dnsServerDomains);
|
||||
}
|
||||
if (context.ProtectDomainList.Count > 0)
|
||||
{
|
||||
AddDnsServers(directDNSAddress, context.ProtectDomainList, true);
|
||||
}
|
||||
|
||||
if (simpleDNSItem.FakeIP == true)
|
||||
{
|
||||
var fakeIPMatchDomain = new HashSet<string>(proxyDomainList);
|
||||
@@ -291,7 +294,7 @@ public partial class CoreConfigV2rayService
|
||||
if (fakeIPMatchDomain.Count > 0)
|
||||
{
|
||||
GenFakeDns();
|
||||
AddDnsServers(["fakedns"], fakeIPMatchDomain.ToList());
|
||||
AddDnsServers(["fakedns"], fakeIPMatchDomain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,10 +303,6 @@ public partial class CoreConfigV2rayService
|
||||
AddDnsServers(remoteDNSAddress, proxyGeositeList);
|
||||
AddDnsServers(directDNSAddress, directGeositeList, true);
|
||||
AddDnsServers(directDNSAddress, expectedDomainList, true, expectedIPs);
|
||||
if (dnsServerDomains.Count > 0)
|
||||
{
|
||||
AddDnsServers(bootstrapDNSAddress, dnsServerDomains);
|
||||
}
|
||||
|
||||
var useDirectDns = false;
|
||||
|
||||
@@ -345,7 +344,7 @@ public partial class CoreConfigV2rayService
|
||||
return addresses.Count > 0 ? addresses : new List<string> { defaultAddress };
|
||||
}
|
||||
|
||||
static DnsServer4Ray CreateDnsServer(string dnsAddress, List<string> domains, List<string>? expectedIPs = null)
|
||||
static DnsServer4Ray CreateDnsServer(string dnsAddress, HashSet<string> domains, HashSet<string>? expectedIPs = null)
|
||||
{
|
||||
var (domain, scheme, port, path) = Utils.ParseUrl(dnsAddress);
|
||||
var domainFinal = dnsAddress;
|
||||
@@ -365,13 +364,13 @@ public partial class CoreConfigV2rayService
|
||||
address = domainFinal,
|
||||
port = portFinal,
|
||||
skipFallback = true,
|
||||
domains = domains.Count > 0 ? domains : null,
|
||||
expectedIPs = expectedIPs?.Count > 0 ? expectedIPs : null
|
||||
domains = domains.Count > 0 ? domains.ToList() : null,
|
||||
expectedIPs = expectedIPs?.Count > 0 ? expectedIPs.ToList() : null
|
||||
};
|
||||
return dnsServer;
|
||||
}
|
||||
|
||||
void AddDnsServers(List<string> dnsAddresses, List<string> domains, bool isDirectDns = false, List<string>? expectedIPs = null)
|
||||
void AddDnsServers(List<string> dnsAddresses, HashSet<string> domains, bool isDirectDns = false, HashSet<string>? expectedIPs = null)
|
||||
{
|
||||
if (domains.Count <= 0)
|
||||
{
|
||||
|
||||
@@ -297,7 +297,8 @@ public partial class CoreConfigV2rayService
|
||||
secretKey = _node.Password,
|
||||
reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(s => s.Trim()).Select(int.Parse).ToList(),
|
||||
mtu = protocolExtra.WgMtu > 0 ? protocolExtra.WgMtu : Global.TunMtus.First(),
|
||||
peers = [peer]
|
||||
remoteDNS = Utils.String2List(protocolExtra.WgDns)?.Select(s => s.Trim()).ToList(),
|
||||
peers = [peer],
|
||||
};
|
||||
outbound.settings = setting;
|
||||
outbound.settings.vnext = null;
|
||||
|
||||
@@ -70,6 +70,9 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
|
||||
[Reactive]
|
||||
public partial int WgMtu { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public partial string WgDns { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public partial bool Uot { get; set; }
|
||||
|
||||
@@ -310,6 +313,7 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
|
||||
WgInterfaceAddress = protocolExtra.WgInterfaceAddress ?? string.Empty;
|
||||
WgReserved = protocolExtra.WgReserved ?? string.Empty;
|
||||
WgMtu = protocolExtra.WgMtu ?? 1280;
|
||||
WgDns = protocolExtra.WgDns ?? string.Empty;
|
||||
Uot = protocolExtra.Uot ?? false;
|
||||
CongestionControl = protocolExtra.CongestionControl ?? string.Empty;
|
||||
InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null;
|
||||
@@ -431,6 +435,7 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
|
||||
WgInterfaceAddress = WgInterfaceAddress.NullIfEmpty(),
|
||||
WgReserved = WgReserved.NullIfEmpty(),
|
||||
WgMtu = WgMtu >= 576 ? WgMtu : null,
|
||||
WgDns = WgDns.NullIfEmpty(),
|
||||
Uot = Uot ? true : null,
|
||||
CongestionControl = CongestionControl.NullIfEmpty(),
|
||||
InsecureConcurrency = InsecureConcurrency > 0 ? InsecureConcurrency : null,
|
||||
|
||||
@@ -580,7 +580,7 @@
|
||||
Grid.Row="2"
|
||||
ColumnDefinitions="300,Auto"
|
||||
IsVisible="False"
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
@@ -649,7 +649,7 @@
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
PlaceholderText="Ipv4,Ipv6" />
|
||||
PlaceholderText="Ipv4, Ipv6" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="6"
|
||||
@@ -665,6 +665,21 @@
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left"
|
||||
PlaceholderText="1280" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="7"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbDNS}" />
|
||||
<TextBox
|
||||
x:Name="txtDns"
|
||||
Grid.Row="7"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left"
|
||||
PlaceholderText="1.1.1.1, 2606:4700:4700::1111" />
|
||||
</Grid>
|
||||
<Grid
|
||||
x:Name="gridAnytls"
|
||||
|
||||
@@ -125,6 +125,7 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
|
||||
this.Bind(ViewModel, vm => vm.WgReserved, v => v.txtPath9.Text).DisposeWith(currentTypeDisposables);
|
||||
this.Bind(ViewModel, vm => vm.WgInterfaceAddress, v => v.txtRequestHost9.Text).DisposeWith(currentTypeDisposables);
|
||||
this.Bind(ViewModel, vm => vm.WgMtu, v => v.txtShortId9.Text).DisposeWith(currentTypeDisposables);
|
||||
this.Bind(ViewModel, vm => vm.WgDns, v => v.txtDns.Text).DisposeWith(currentTypeDisposables);
|
||||
break;
|
||||
|
||||
case EConfigType.Anytls:
|
||||
|
||||
@@ -748,6 +748,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="300" />
|
||||
@@ -830,7 +831,7 @@
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
materialDesign:HintAssist.Hint="Ipv4,Ipv6"
|
||||
materialDesign:HintAssist.Hint="Ipv4, Ipv6"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
|
||||
<TextBlock
|
||||
@@ -849,6 +850,23 @@
|
||||
HorizontalAlignment="Left"
|
||||
materialDesign:HintAssist.Hint="1280"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="7"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbDNS}" />
|
||||
<TextBox
|
||||
x:Name="txtDns"
|
||||
Grid.Row="7"
|
||||
Grid.Column="1"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left"
|
||||
materialDesign:HintAssist.Hint="1.1.1.1, 2606:4700:4700::1111"
|
||||
Style="{StaticResource DefTextBox}" />
|
||||
</Grid>
|
||||
<Grid
|
||||
x:Name="gridAnytls"
|
||||
|
||||
@@ -124,6 +124,7 @@ public partial class AddServerWindow
|
||||
this.Bind(ViewModel, vm => vm.WgReserved, v => v.txtPath9.Text).DisposeWith(currentTypeDisposables);
|
||||
this.Bind(ViewModel, vm => vm.WgInterfaceAddress, v => v.txtRequestHost9.Text).DisposeWith(currentTypeDisposables);
|
||||
this.Bind(ViewModel, vm => vm.WgMtu, v => v.txtShortId9.Text).DisposeWith(currentTypeDisposables);
|
||||
this.Bind(ViewModel, vm => vm.WgDns, v => v.txtDns.Text).DisposeWith(currentTypeDisposables);
|
||||
break;
|
||||
|
||||
case EConfigType.Anytls:
|
||||
|
||||
Reference in New Issue
Block a user