mirror of
https://github.com/2dust/v2rayN.git
synced 2026-11-02 18:45:00 +03:00
@@ -499,6 +499,20 @@ public class Global
|
||||
"localhost"
|
||||
];
|
||||
|
||||
public static readonly List<string> DomainDNS4PreResolveAddress =
|
||||
[
|
||||
"",
|
||||
"https://doh.pub/dns-query",
|
||||
"https://dns.alidns.com/dns-query",
|
||||
"119.29.29.29",
|
||||
"223.5.5.5",
|
||||
"localhost",
|
||||
"https://cloudflare-dns.com/dns-query",
|
||||
"https://dns.google/dns-query",
|
||||
"8.8.8.8",
|
||||
"1.1.1.1",
|
||||
];
|
||||
|
||||
public static readonly List<LanguageOption> LanguageOptions =
|
||||
[
|
||||
new("zh-Hans", "简体中文"),
|
||||
|
||||
@@ -1713,11 +1713,16 @@ public static class ConfigHandler
|
||||
}
|
||||
|
||||
var subFilter = string.Empty;
|
||||
var preResolver = string.Empty;
|
||||
if (isSub && subid.IsNotEmpty())
|
||||
{
|
||||
subFilter = (await AppManager.Instance.GetSubItem(subid))?.Filter ?? "";
|
||||
preResolver = (await AppManager.Instance.GetSubItem(subid))?.PreResolver ?? "";
|
||||
}
|
||||
|
||||
var dnsClient = preResolver.IsNullOrEmpty() ? null : EndpointResolveManager.BuildDnsClient(preResolver);
|
||||
var dnsCache = new Dictionary<string, string>();
|
||||
|
||||
var countServers = 0;
|
||||
List<ProfileItem> lstAdd = [];
|
||||
var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty());
|
||||
@@ -1753,6 +1758,21 @@ public static class ConfigHandler
|
||||
profileItem.Subid = subid;
|
||||
profileItem.IsSub = isSub;
|
||||
|
||||
if (dnsClient is not null && profileItem.Address.IsNotEmpty())
|
||||
{
|
||||
if (!dnsCache.TryGetValue(profileItem.Address, out var resolvedAddress))
|
||||
{
|
||||
var ipList = await dnsClient.ResolveIpv46Async(profileItem.Address);
|
||||
resolvedAddress = ipList.FirstOrDefault()?.ToString() ?? profileItem.Address;
|
||||
dnsCache[profileItem.Address] = resolvedAddress;
|
||||
}
|
||||
if (!resolvedAddress.IsNullOrEmpty())
|
||||
{
|
||||
profileItem.Address = resolvedAddress;
|
||||
}
|
||||
// TODO: Override the host and sni if empty
|
||||
}
|
||||
|
||||
var addStatus = profileItem.ConfigType switch
|
||||
{
|
||||
EConfigType.VMess => await AddVMessServer(config, profileItem, false),
|
||||
@@ -2297,6 +2317,7 @@ public static class ConfigHandler
|
||||
item.PreSocksPort = subItem.PreSocksPort;
|
||||
item.Memo = subItem.Memo;
|
||||
item.CustomCoreType = subItem.CustomCoreType;
|
||||
item.PreResolver = subItem.PreResolver;
|
||||
}
|
||||
|
||||
if (item.Id.IsNullOrEmpty())
|
||||
|
||||
@@ -0,0 +1,523 @@
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public class EndpointResolveManager
|
||||
{
|
||||
public static IDnsResolver BuildDnsClient(string dnsServer)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dnsServer))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dnsServer));
|
||||
}
|
||||
|
||||
if (dnsServer is "local" or "localhost" or "system" or "dhcp" ||
|
||||
dnsServer.StartsWith("127.0.0.1") ||
|
||||
dnsServer.StartsWith("::1"))
|
||||
{
|
||||
return new SystemDnsResolver();
|
||||
}
|
||||
|
||||
var (domain, scheme, port, path) = Utils.ParseUrl(dnsServer);
|
||||
|
||||
if (string.IsNullOrEmpty(scheme))
|
||||
{
|
||||
return new UdpDnsResolver(new IPEndPoint(IPAddress.Parse(domain), 53));
|
||||
}
|
||||
|
||||
var processedScheme = scheme.ToLowerInvariant().Replace("+local", "");
|
||||
|
||||
switch (processedScheme)
|
||||
{
|
||||
case "udp":
|
||||
return new UdpDnsResolver(new IPEndPoint(IPAddress.Parse(domain), port != 0 ? port : 53));
|
||||
|
||||
case "tcp":
|
||||
return new TcpDnsResolver(new IPEndPoint(IPAddress.Parse(domain), port != 0 ? port : 53));
|
||||
|
||||
case "doh" or "https":
|
||||
{
|
||||
var uri = new Uri($"https://{domain}:{(port != 0 ? port : 443)}{path}");
|
||||
var httpClient = new HttpClient();
|
||||
return new DohDnsResolver(httpClient, uri);
|
||||
}
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported DNS scheme: {scheme}");
|
||||
}
|
||||
}
|
||||
|
||||
public class DnsWire
|
||||
{
|
||||
#region Build DNS Query
|
||||
|
||||
public static byte[] BuildQuery(string host, ushort queryType)
|
||||
{
|
||||
if (queryType is not (1 or 28))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(queryType));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
throw new ArgumentException("Host is empty.", nameof(host));
|
||||
}
|
||||
|
||||
host = host.TrimEnd('.');
|
||||
|
||||
if (host.Length == 0)
|
||||
{
|
||||
host = ".";
|
||||
}
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
|
||||
// Header
|
||||
WriteUInt16(stream, 0);
|
||||
WriteUInt16(stream, 0x0100);
|
||||
WriteUInt16(stream, 1);
|
||||
WriteUInt16(stream, 0);
|
||||
WriteUInt16(stream, 0);
|
||||
WriteUInt16(stream, 0);
|
||||
|
||||
// Question: QNAME
|
||||
if (host != ".")
|
||||
{
|
||||
var labels = host.Split('.');
|
||||
foreach (var label in labels)
|
||||
{
|
||||
if (label.Length is < 1 or > 63)
|
||||
{
|
||||
throw new ArgumentException($"Invalid DNS label length: {label.Length}.", nameof(host));
|
||||
}
|
||||
|
||||
foreach (var c in label)
|
||||
{
|
||||
if (c > 0x7F)
|
||||
{
|
||||
throw new ArgumentException("Non-ASCII domain names must be converted to A-labels first.",
|
||||
nameof(host));
|
||||
}
|
||||
}
|
||||
|
||||
stream.WriteByte((byte)label.Length);
|
||||
|
||||
var labelBytes = Encoding.ASCII.GetBytes(label);
|
||||
stream.Write(labelBytes, 0, labelBytes.Length);
|
||||
}
|
||||
}
|
||||
|
||||
stream.WriteByte(0);
|
||||
|
||||
// Question: QTYPE & QCLASS
|
||||
WriteUInt16(stream, queryType);
|
||||
WriteUInt16(stream, 1);
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static void WriteUInt16(Stream stream, ushort value)
|
||||
{
|
||||
stream.WriteByte((byte)(value >> 8));
|
||||
stream.WriteByte((byte)(value & 0xFF));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parse DNS Response
|
||||
|
||||
public static List<IPAddress> ParseAddresses(
|
||||
ReadOnlySpan<byte> message,
|
||||
ushort expectedType)
|
||||
{
|
||||
return ParseResponse(message, expectedType).Addresses;
|
||||
}
|
||||
|
||||
public static DnsResponse ParseResponse(
|
||||
ReadOnlySpan<byte> message,
|
||||
ushort expectedType)
|
||||
{
|
||||
if (expectedType is not (1 or 28))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(expectedType));
|
||||
}
|
||||
|
||||
if (message.Length < 12)
|
||||
{
|
||||
throw new InvalidDataException("DNS message is too short.");
|
||||
}
|
||||
|
||||
var flags = ReadUInt16(message, 2);
|
||||
var questionCount = ReadUInt16(message, 4);
|
||||
var answerCount = ReadUInt16(message, 6);
|
||||
|
||||
// QR = 1
|
||||
if ((flags & 0x8000) == 0)
|
||||
{
|
||||
throw new InvalidDataException("Not a DNS response.");
|
||||
}
|
||||
|
||||
// RCODE
|
||||
var rcode = flags & 0x000F;
|
||||
if (rcode != 0)
|
||||
{
|
||||
return new DnsResponse([], null);
|
||||
}
|
||||
|
||||
var offset = 12;
|
||||
|
||||
// Parse the Question section to obtain the query name.
|
||||
string? queryName = null;
|
||||
for (var i = 0; i < questionCount; i++)
|
||||
{
|
||||
var name = ReadName(message, offset, out offset);
|
||||
queryName ??= NormalizeName(name);
|
||||
|
||||
if (offset + 4 > message.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS question.");
|
||||
}
|
||||
|
||||
offset += 4; // QTYPE + QCLASS
|
||||
}
|
||||
|
||||
var addresses = new List<IPAddress>();
|
||||
string? cname = null;
|
||||
|
||||
for (var i = 0; i < answerCount; i++)
|
||||
{
|
||||
// NAME
|
||||
var ownerName = ReadName(message, offset, out offset);
|
||||
|
||||
if (offset + 10 > message.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS answer.");
|
||||
}
|
||||
|
||||
var type = ReadUInt16(message, offset);
|
||||
var @class = ReadUInt16(message, offset + 2);
|
||||
// TTL
|
||||
var rdLength = ReadUInt16(message, offset + 8);
|
||||
|
||||
offset += 10;
|
||||
|
||||
if (offset + rdLength > message.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS RDATA.");
|
||||
}
|
||||
|
||||
if (@class != 1)
|
||||
{
|
||||
offset += rdLength;
|
||||
continue;
|
||||
}
|
||||
|
||||
var ownerMatchesQuery = NamesEqual(ownerName, queryName);
|
||||
var ownerMatchesCname = NamesEqual(ownerName, cname);
|
||||
|
||||
if (type == expectedType)
|
||||
{
|
||||
if (ownerMatchesQuery || ownerMatchesCname)
|
||||
{
|
||||
if (expectedType == 1 && rdLength == 4)
|
||||
{
|
||||
addresses.Add(new IPAddress(
|
||||
message.Slice(offset, 4)));
|
||||
}
|
||||
else if (expectedType == 28 && rdLength == 16)
|
||||
{
|
||||
addresses.Add(new IPAddress(
|
||||
message.Slice(offset, 16)));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == 5 && ownerMatchesQuery && cname is null)
|
||||
{
|
||||
cname = ReadName(message, offset, out _);
|
||||
}
|
||||
|
||||
offset += rdLength;
|
||||
}
|
||||
|
||||
return new DnsResponse(addresses, cname);
|
||||
}
|
||||
|
||||
public static async Task<List<IPAddress>> ResolveWithCnameAsync(
|
||||
string host,
|
||||
bool ipv6,
|
||||
Func<string, ushort, CancellationToken, Task<byte[]>> queryAsync,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
throw new ArgumentException("Host is empty.", nameof(host));
|
||||
}
|
||||
|
||||
var queryType = ipv6 ? (ushort)28 : (ushort)1;
|
||||
var currentName = host.TrimEnd('.');
|
||||
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
const int maxHops = 8;
|
||||
|
||||
for (var hop = 0; hop < maxHops; hop++)
|
||||
{
|
||||
if (!visited.Add(currentName))
|
||||
{
|
||||
// CNAME loop detected.
|
||||
return [];
|
||||
}
|
||||
|
||||
var response = await queryAsync(currentName, queryType, cancellationToken);
|
||||
var result = ParseResponse(response, queryType);
|
||||
|
||||
if (result.Addresses.Count > 0)
|
||||
{
|
||||
return result.Addresses;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(result.Cname))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
currentName = result.Cname.TrimEnd('.');
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public static string ReadName(
|
||||
ReadOnlySpan<byte> message,
|
||||
int offset,
|
||||
out int nextOffset)
|
||||
{
|
||||
var name = new StringBuilder();
|
||||
var position = offset;
|
||||
var endOffset = -1;
|
||||
var pointerCount = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if ((uint)position >= (uint)message.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS name.");
|
||||
}
|
||||
|
||||
var length = message[position];
|
||||
|
||||
// End of name.
|
||||
if (length == 0)
|
||||
{
|
||||
position++;
|
||||
if (endOffset < 0)
|
||||
{
|
||||
endOffset = position;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Compression pointer.
|
||||
if ((length & 0xC0) == 0xC0)
|
||||
{
|
||||
if (position + 1 >= message.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS pointer.");
|
||||
}
|
||||
|
||||
if (++pointerCount > 64)
|
||||
{
|
||||
throw new InvalidDataException("Too many DNS compression pointers.");
|
||||
}
|
||||
|
||||
if (endOffset < 0)
|
||||
{
|
||||
endOffset = position + 2;
|
||||
}
|
||||
|
||||
var pointer = ((length & 0x3F) << 8) | message[position + 1];
|
||||
position = pointer;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reserved label types.
|
||||
if ((length & 0xC0) != 0)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS label.");
|
||||
}
|
||||
|
||||
position++;
|
||||
if (position + length > message.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS label length.");
|
||||
}
|
||||
|
||||
if (name.Length > 0)
|
||||
{
|
||||
name.Append('.');
|
||||
}
|
||||
|
||||
name.Append(Encoding.ASCII.GetString(message.Slice(position, length)));
|
||||
position += length;
|
||||
}
|
||||
|
||||
nextOffset = endOffset;
|
||||
return name.ToString();
|
||||
}
|
||||
|
||||
public sealed record DnsResponse(List<IPAddress> Addresses, string? Cname);
|
||||
|
||||
private static string? NormalizeName(string name)
|
||||
{
|
||||
name = name.TrimEnd('.');
|
||||
return name.Length == 0 ? null : name;
|
||||
}
|
||||
|
||||
private static bool NamesEqual(string? left, string? right)
|
||||
{
|
||||
return left is not null &&
|
||||
right is not null &&
|
||||
string.Equals(
|
||||
left.TrimEnd('.'),
|
||||
right.TrimEnd('.'),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static ushort ReadUInt16(
|
||||
ReadOnlySpan<byte> data,
|
||||
int offset)
|
||||
{
|
||||
return (ushort)(
|
||||
(data[offset] << 8) |
|
||||
data[offset + 1]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public interface IDnsResolver
|
||||
{
|
||||
public Task<List<IPAddress>> ResolveAsync(
|
||||
string host,
|
||||
bool ipv6,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public async Task<List<IPAddress>> ResolveIpv46Async(
|
||||
string host,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var ipv4Task = ResolveAsync(host, false, cancellationToken);
|
||||
var ipv6Task = ResolveAsync(host, true, cancellationToken);
|
||||
var results = await Task.WhenAll(ipv4Task, ipv6Task);
|
||||
return results.SelectMany(x => x).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class UdpDnsResolver(IPEndPoint dnsServer) : IDnsResolver
|
||||
{
|
||||
public async Task<List<IPAddress>> ResolveAsync(
|
||||
string host,
|
||||
bool ipv6,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DnsWire.ResolveWithCnameAsync(
|
||||
host,
|
||||
ipv6,
|
||||
async (name, queryType, ct) =>
|
||||
{
|
||||
var query = DnsWire.BuildQuery(name, queryType);
|
||||
using var udpClient = new UdpClient();
|
||||
udpClient.Connect(dnsServer);
|
||||
await udpClient.SendAsync(query, query.Length);
|
||||
var result = await udpClient.ReceiveAsync(ct);
|
||||
return result.Buffer;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class TcpDnsResolver(IPEndPoint dnsServer) : IDnsResolver
|
||||
{
|
||||
public async Task<List<IPAddress>> ResolveAsync(
|
||||
string host,
|
||||
bool ipv6,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DnsWire.ResolveWithCnameAsync(
|
||||
host,
|
||||
ipv6,
|
||||
async (name, queryType, ct) =>
|
||||
{
|
||||
var query = DnsWire.BuildQuery(name, queryType);
|
||||
using var tcpClient = new TcpClient();
|
||||
await tcpClient.ConnectAsync(dnsServer.Address, dnsServer.Port, ct);
|
||||
await using var networkStream = tcpClient.GetStream();
|
||||
await networkStream.WriteAsync(query, 0, query.Length, ct);
|
||||
// Read the length of the response (2 bytes)
|
||||
var lengthBuffer = new byte[2];
|
||||
await networkStream.ReadExactlyAsync(lengthBuffer, 0, 2, ct);
|
||||
var responseLength = (lengthBuffer[0] << 8) | lengthBuffer[1];
|
||||
// Read the actual response
|
||||
var responseBuffer = new byte[responseLength];
|
||||
await networkStream.ReadExactlyAsync(responseBuffer, 0, responseLength, ct);
|
||||
return responseBuffer;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class DohDnsResolver(HttpClient httpClient, Uri dohServer) : IDnsResolver
|
||||
{
|
||||
public async Task<List<IPAddress>> ResolveAsync(
|
||||
string host,
|
||||
bool ipv6,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DnsWire.ResolveWithCnameAsync(
|
||||
host,
|
||||
ipv6,
|
||||
async (name, queryType, ct) =>
|
||||
{
|
||||
var query = DnsWire.BuildQuery(name, queryType);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, dohServer);
|
||||
request.Content = new ByteArrayContent(query)
|
||||
{
|
||||
Headers =
|
||||
{
|
||||
ContentType = new MediaTypeHeaderValue("application/dns-message"),
|
||||
},
|
||||
};
|
||||
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/dns-message"));
|
||||
|
||||
using var response =
|
||||
await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return await response.Content.ReadAsByteArrayAsync(ct);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class SystemDnsResolver : IDnsResolver
|
||||
{
|
||||
public async Task<List<IPAddress>> ResolveAsync(
|
||||
string host,
|
||||
bool ipv6,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var addresses = await Dns.GetHostAddressesAsync(host, cancellationToken);
|
||||
return addresses.Where(ip => (!ipv6 && ip.AddressFamily == AddressFamily.InterNetwork) ||
|
||||
(ipv6 && ip.AddressFamily == AddressFamily.InterNetworkV6))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<IPAddress>> ResolveIpv46Async(
|
||||
string host,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var addresses = await Dns.GetHostAddressesAsync(host, cancellationToken);
|
||||
return addresses.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,4 +36,6 @@ public class SubItem
|
||||
public string? Memo { get; set; }
|
||||
|
||||
public ECoreType? CustomCoreType { get; set; }
|
||||
|
||||
public string? PreResolver { get; set; }
|
||||
}
|
||||
|
||||
Generated
+9
@@ -528,6 +528,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Pre-resolve server domains 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string LvPreResolve {
|
||||
get {
|
||||
return ResourceManager.GetString("LvPreResolve", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Previous proxy remarks 的本地化字符串。
|
||||
/// </summary>
|
||||
|
||||
@@ -1911,4 +1911,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="TbWarp" xml:space="preserve">
|
||||
<value>Warp</value>
|
||||
</data>
|
||||
<data name="LvPreResolve" xml:space="preserve">
|
||||
<value>Pre-resolve server domains</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1903,4 +1903,7 @@
|
||||
<data name="TbWarp" xml:space="preserve">
|
||||
<value>Warp</value>
|
||||
</data>
|
||||
<data name="LvPreResolve" xml:space="preserve">
|
||||
<value>预解析节点域名</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -34,7 +34,7 @@
|
||||
</StackPanel>
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,400,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
|
||||
<Grid ColumnDefinitions="Auto,400,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
@@ -187,18 +187,33 @@
|
||||
Grid.Row="8"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.LvPreResolve}" />
|
||||
<ComboBox
|
||||
x:Name="cmbPreResolve"
|
||||
Grid.Row="8"
|
||||
Grid.Column="1"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
IsEditable="True"
|
||||
PlaceholderText="{x:Static resx:ResUI.SubUrlTips}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="9"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Top"
|
||||
Text="{x:Static resx:ResUI.LvRequestHeaders}" />
|
||||
<StackPanel Grid.Row="8" Grid.Column="1">
|
||||
<StackPanel Grid.Row="9" Grid.Column="1">
|
||||
<TextBox
|
||||
x:Name="txtRequestHeaders"
|
||||
Margin="{StaticResource Margin4}"
|
||||
AcceptsReturn="True"
|
||||
MinLines="4"
|
||||
MaxLines="8"
|
||||
Classes="TextArea"
|
||||
TextWrapping="Wrap"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto" />
|
||||
MaxLines="8"
|
||||
MinLines="4"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
Margin="{StaticResource Margin4}"
|
||||
Text="{x:Static resx:ResUI.SubRequestHeadersTips}"
|
||||
@@ -206,28 +221,28 @@
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="9"
|
||||
Grid.Row="10"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.LvSort}" />
|
||||
<TextBox
|
||||
x:Name="txtSort"
|
||||
Grid.Row="9"
|
||||
Grid.Row="10"
|
||||
Grid.Column="1"
|
||||
Width="100"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="10"
|
||||
Grid.Row="11"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.LvPrevProfile}" />
|
||||
<TextBox
|
||||
x:Name="txtPrevProfile"
|
||||
Grid.Row="10"
|
||||
Grid.Row="11"
|
||||
Grid.Column="1"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -235,20 +250,20 @@
|
||||
ToolTip.Tip="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
||||
<Button
|
||||
x:Name="btnSelectPrevProfile"
|
||||
Grid.Row="10"
|
||||
Grid.Row="11"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Content="{x:Static resx:ResUI.TbSelectProfile}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="11"
|
||||
Grid.Row="12"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.LvNextProfile}" />
|
||||
<TextBox
|
||||
x:Name="txtNextProfile"
|
||||
Grid.Row="11"
|
||||
Grid.Row="12"
|
||||
Grid.Column="1"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -256,34 +271,34 @@
|
||||
ToolTip.Tip="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
||||
<Button
|
||||
x:Name="btnSelectNextProfile"
|
||||
Grid.Row="11"
|
||||
Grid.Row="12"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Content="{x:Static resx:ResUI.TbSelectProfile}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="12"
|
||||
Grid.Row="13"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.LvCustomCoreType}" />
|
||||
<ComboBox
|
||||
x:Name="cmbCustomCoreType"
|
||||
Grid.Row="12"
|
||||
Grid.Row="13"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="13"
|
||||
Grid.Row="14"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
|
||||
<TextBox
|
||||
x:Name="txtPreSocksPort"
|
||||
Grid.Row="13"
|
||||
Grid.Row="14"
|
||||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
@@ -292,14 +307,14 @@
|
||||
ToolTip.Tip="{x:Static resx:ResUI.TipPreSocksPort}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="14"
|
||||
Grid.Row="15"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.LvMemo}" />
|
||||
<TextBox
|
||||
x:Name="txtMemo"
|
||||
Grid.Row="14"
|
||||
Grid.Row="15"
|
||||
Grid.Column="1"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
|
||||
@@ -12,6 +12,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
|
||||
btnCancel.Click += (s, e) => Close();
|
||||
|
||||
cmbConvertTarget.ItemsSource = Global.SubConvertTargets;
|
||||
cmbPreResolve.ItemsSource = Global.DomainDNS4PreResolveAddress;
|
||||
cmbCustomCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList().AppendEmpty();
|
||||
|
||||
this.WhenActivated(disposables =>
|
||||
@@ -31,6 +32,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.PreSocksPort, v => v.txtPreSocksPort.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Memo, v => v.txtMemo.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.CustomCoreType, v => v.cmbCustomCoreType.SelectedValue).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.PreResolver, v => v.cmbPreResolve.Text).DisposeWith(disposables);
|
||||
|
||||
this.BindCommand(ViewModel, vm => vm.SelectPrevProfileCmd, v => v.btnSelectPrevProfile).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.SelectNextProfileCmd, v => v.btnSelectNextProfile).DisposeWith(disposables);
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@@ -232,15 +233,31 @@
|
||||
Grid.Row="8"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.LvPreResolve}" />
|
||||
<ComboBox
|
||||
x:Name="cmbPreResolve"
|
||||
Grid.Row="8"
|
||||
Grid.Column="1"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
materialDesign:HintAssist.Hint="{x:Static resx:ResUI.SubUrlTips}"
|
||||
IsEditable="True"
|
||||
Style="{StaticResource DefComboBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="9"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Top"
|
||||
Text="{x:Static resx:ResUI.LvRequestHeaders}" />
|
||||
<StackPanel Grid.Row="8" Grid.Column="1">
|
||||
<StackPanel Grid.Row="9" Grid.Column="1">
|
||||
<TextBox
|
||||
x:Name="txtRequestHeaders"
|
||||
Margin="{StaticResource Margin4}"
|
||||
AcceptsReturn="True"
|
||||
MinLines="4"
|
||||
MaxLines="8"
|
||||
MinLines="4"
|
||||
Style="{StaticResource MyOutlinedTextBox}"
|
||||
TextWrapping="Wrap"
|
||||
VerticalScrollBarVisibility="Auto" />
|
||||
@@ -251,7 +268,7 @@
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="9"
|
||||
Grid.Row="10"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -259,7 +276,7 @@
|
||||
Text="{x:Static resx:ResUI.LvSort}" />
|
||||
<TextBox
|
||||
x:Name="txtSort"
|
||||
Grid.Row="9"
|
||||
Grid.Row="10"
|
||||
Grid.Column="1"
|
||||
Width="100"
|
||||
Margin="{StaticResource Margin4}"
|
||||
@@ -269,7 +286,7 @@
|
||||
Style="{StaticResource MyOutlinedTextBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="10"
|
||||
Grid.Row="11"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -277,7 +294,7 @@
|
||||
Text="{x:Static resx:ResUI.LvPrevProfile}" />
|
||||
<TextBox
|
||||
x:Name="txtPrevProfile"
|
||||
Grid.Row="10"
|
||||
Grid.Row="11"
|
||||
Grid.Column="1"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -287,7 +304,7 @@
|
||||
ToolTip="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
||||
<Button
|
||||
x:Name="btnSelectPrevProfile"
|
||||
Grid.Row="10"
|
||||
Grid.Row="11"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -295,7 +312,7 @@
|
||||
Style="{StaticResource DefButton}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="11"
|
||||
Grid.Row="12"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -303,7 +320,7 @@
|
||||
Text="{x:Static resx:ResUI.LvNextProfile}" />
|
||||
<TextBox
|
||||
x:Name="txtNextProfile"
|
||||
Grid.Row="11"
|
||||
Grid.Row="12"
|
||||
Grid.Column="1"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -313,7 +330,7 @@
|
||||
ToolTip="{x:Static resx:ResUI.LvPrevProfileTip}" />
|
||||
<Button
|
||||
x:Name="btnSelectNextProfile"
|
||||
Grid.Row="11"
|
||||
Grid.Row="12"
|
||||
Grid.Column="2"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -321,7 +338,7 @@
|
||||
Style="{StaticResource DefButton}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="12"
|
||||
Grid.Row="13"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -329,14 +346,14 @@
|
||||
Text="{x:Static resx:ResUI.LvCustomCoreType}" />
|
||||
<ComboBox
|
||||
x:Name="cmbCustomCoreType"
|
||||
Grid.Row="12"
|
||||
Grid.Row="13"
|
||||
Grid.Column="1"
|
||||
Margin="{StaticResource Margin4}"
|
||||
MaxDropDownHeight="1000"
|
||||
Style="{StaticResource MyOutlinedTextComboBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="13"
|
||||
Grid.Row="14"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -344,7 +361,7 @@
|
||||
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
|
||||
<TextBox
|
||||
x:Name="txtPreSocksPort"
|
||||
Grid.Row="13"
|
||||
Grid.Row="14"
|
||||
Grid.Column="1"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Left"
|
||||
@@ -354,7 +371,7 @@
|
||||
ToolTip="{x:Static resx:ResUI.TipPreSocksPort}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="14"
|
||||
Grid.Row="15"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
@@ -362,7 +379,7 @@
|
||||
Text="{x:Static resx:ResUI.LvMemo}" />
|
||||
<TextBox
|
||||
x:Name="txtMemo"
|
||||
Grid.Row="14"
|
||||
Grid.Row="15"
|
||||
Grid.Column="1"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
|
||||
@@ -9,6 +9,7 @@ public partial class SubEditWindow
|
||||
Loaded += Window_Loaded;
|
||||
|
||||
cmbConvertTarget.ItemsSource = Global.SubConvertTargets;
|
||||
cmbPreResolve.ItemsSource = Global.DomainDNS4PreResolveAddress;
|
||||
cmbCustomCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList().AppendEmpty();
|
||||
|
||||
this.WhenActivated(disposables =>
|
||||
@@ -28,6 +29,7 @@ public partial class SubEditWindow
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.PreSocksPort, v => v.txtPreSocksPort.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Memo, v => v.txtMemo.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.CustomCoreType, v => v.cmbCustomCoreType.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.PreResolver, v => v.cmbPreResolve.Text).DisposeWith(disposables);
|
||||
|
||||
this.BindCommand(ViewModel, vm => vm.SelectPrevProfileCmd, v => v.btnSelectPrevProfile).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.SelectNextProfileCmd, v => v.btnSelectNextProfile).DisposeWith(disposables);
|
||||
|
||||
Reference in New Issue
Block a user