Profile pre-resolver (#10220)

* Profile pre-resolver

* Try cname

* UI
This commit is contained in:
DHR60
2026-11-02 07:02:44 +00:00
committed by GitHub
parent 4c4449e74d
commit 19850f92be
11 changed files with 647 additions and 36 deletions
+14
View File
@@ -499,6 +499,20 @@ public class Global
"localhost" "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 = public static readonly List<LanguageOption> LanguageOptions =
[ [
new("zh-Hans", "简体中文"), new("zh-Hans", "简体中文"),
@@ -1713,11 +1713,16 @@ public static class ConfigHandler
} }
var subFilter = string.Empty; var subFilter = string.Empty;
var preResolver = string.Empty;
if (isSub && subid.IsNotEmpty()) if (isSub && subid.IsNotEmpty())
{ {
subFilter = (await AppManager.Instance.GetSubItem(subid))?.Filter ?? ""; 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; var countServers = 0;
List<ProfileItem> lstAdd = []; List<ProfileItem> lstAdd = [];
var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty()); var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty());
@@ -1753,6 +1758,21 @@ public static class ConfigHandler
profileItem.Subid = subid; profileItem.Subid = subid;
profileItem.IsSub = isSub; 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 var addStatus = profileItem.ConfigType switch
{ {
EConfigType.VMess => await AddVMessServer(config, profileItem, false), EConfigType.VMess => await AddVMessServer(config, profileItem, false),
@@ -2297,6 +2317,7 @@ public static class ConfigHandler
item.PreSocksPort = subItem.PreSocksPort; item.PreSocksPort = subItem.PreSocksPort;
item.Memo = subItem.Memo; item.Memo = subItem.Memo;
item.CustomCoreType = subItem.CustomCoreType; item.CustomCoreType = subItem.CustomCoreType;
item.PreResolver = subItem.PreResolver;
} }
if (item.Id.IsNullOrEmpty()) 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 string? Memo { get; set; }
public ECoreType? CustomCoreType { get; set; } public ECoreType? CustomCoreType { get; set; }
public string? PreResolver { get; set; }
} }
+9
View File
@@ -528,6 +528,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Pre-resolve server domains 的本地化字符串。
/// </summary>
public static string LvPreResolve {
get {
return ResourceManager.GetString("LvPreResolve", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Previous proxy remarks 的本地化字符串。 /// 查找类似 Previous proxy remarks 的本地化字符串。
/// </summary> /// </summary>
+3
View File
@@ -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"> <data name="TbWarp" xml:space="preserve">
<value>Warp</value> <value>Warp</value>
</data> </data>
<data name="LvPreResolve" xml:space="preserve">
<value>Pre-resolve server domains</value>
</data>
</root> </root>
@@ -1903,4 +1903,7 @@
<data name="TbWarp" xml:space="preserve"> <data name="TbWarp" xml:space="preserve">
<value>Warp</value> <value>Warp</value>
</data> </data>
<data name="LvPreResolve" xml:space="preserve">
<value>预解析节点域名</value>
</data>
</root> </root>
+35 -20
View File
@@ -34,7 +34,7 @@
</StackPanel> </StackPanel>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"> <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 <TextBlock
Grid.Row="0" Grid.Row="0"
@@ -187,18 +187,33 @@
Grid.Row="8" Grid.Row="8"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" 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" VerticalAlignment="Top"
Text="{x:Static resx:ResUI.LvRequestHeaders}" /> Text="{x:Static resx:ResUI.LvRequestHeaders}" />
<StackPanel Grid.Row="8" Grid.Column="1"> <StackPanel Grid.Row="9" Grid.Column="1">
<TextBox <TextBox
x:Name="txtRequestHeaders" x:Name="txtRequestHeaders"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
AcceptsReturn="True" AcceptsReturn="True"
MinLines="4"
MaxLines="8"
Classes="TextArea" Classes="TextArea"
TextWrapping="Wrap" MaxLines="8"
ScrollViewer.VerticalScrollBarVisibility="Auto" /> MinLines="4"
ScrollViewer.VerticalScrollBarVisibility="Auto"
TextWrapping="Wrap" />
<TextBlock <TextBlock
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Text="{x:Static resx:ResUI.SubRequestHeadersTips}" Text="{x:Static resx:ResUI.SubRequestHeadersTips}"
@@ -206,28 +221,28 @@
</StackPanel> </StackPanel>
<TextBlock <TextBlock
Grid.Row="9" Grid.Row="10"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvSort}" /> Text="{x:Static resx:ResUI.LvSort}" />
<TextBox <TextBox
x:Name="txtSort" x:Name="txtSort"
Grid.Row="9" Grid.Row="10"
Grid.Column="1" Grid.Column="1"
Width="100" Width="100"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock <TextBlock
Grid.Row="10" Grid.Row="11"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvPrevProfile}" /> Text="{x:Static resx:ResUI.LvPrevProfile}" />
<TextBox <TextBox
x:Name="txtPrevProfile" x:Name="txtPrevProfile"
Grid.Row="10" Grid.Row="11"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -235,20 +250,20 @@
ToolTip.Tip="{x:Static resx:ResUI.LvPrevProfileTip}" /> ToolTip.Tip="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button <Button
x:Name="btnSelectPrevProfile" x:Name="btnSelectPrevProfile"
Grid.Row="10" Grid.Row="11"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbSelectProfile}" /> Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock <TextBlock
Grid.Row="11" Grid.Row="12"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvNextProfile}" /> Text="{x:Static resx:ResUI.LvNextProfile}" />
<TextBox <TextBox
x:Name="txtNextProfile" x:Name="txtNextProfile"
Grid.Row="11" Grid.Row="12"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -256,34 +271,34 @@
ToolTip.Tip="{x:Static resx:ResUI.LvPrevProfileTip}" /> ToolTip.Tip="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button <Button
x:Name="btnSelectNextProfile" x:Name="btnSelectNextProfile"
Grid.Row="11" Grid.Row="12"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbSelectProfile}" /> Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock <TextBlock
Grid.Row="12" Grid.Row="13"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvCustomCoreType}" /> Text="{x:Static resx:ResUI.LvCustomCoreType}" />
<ComboBox <ComboBox
x:Name="cmbCustomCoreType" x:Name="cmbCustomCoreType"
Grid.Row="12" Grid.Row="13"
Grid.Column="1" Grid.Column="1"
Width="200" Width="200"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock <TextBlock
Grid.Row="13" Grid.Row="14"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" /> Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
<TextBox <TextBox
x:Name="txtPreSocksPort" x:Name="txtPreSocksPort"
Grid.Row="13" Grid.Row="14"
Grid.Column="1" Grid.Column="1"
Width="200" Width="200"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@@ -292,14 +307,14 @@
ToolTip.Tip="{x:Static resx:ResUI.TipPreSocksPort}" /> ToolTip.Tip="{x:Static resx:ResUI.TipPreSocksPort}" />
<TextBlock <TextBlock
Grid.Row="14" Grid.Row="15"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.LvMemo}" /> Text="{x:Static resx:ResUI.LvMemo}" />
<TextBox <TextBox
x:Name="txtMemo" x:Name="txtMemo"
Grid.Row="14" Grid.Row="15"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -12,6 +12,7 @@ public partial class SubEditWindow : WindowBase<SubEditViewModel>
btnCancel.Click += (s, e) => Close(); btnCancel.Click += (s, e) => Close();
cmbConvertTarget.ItemsSource = Global.SubConvertTargets; cmbConvertTarget.ItemsSource = Global.SubConvertTargets;
cmbPreResolve.ItemsSource = Global.DomainDNS4PreResolveAddress;
cmbCustomCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList().AppendEmpty(); cmbCustomCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList().AppendEmpty();
this.WhenActivated(disposables => 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.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.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.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.SelectPrevProfileCmd, v => v.btnSelectPrevProfile).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SelectNextProfileCmd, v => v.btnSelectNextProfile).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SelectNextProfileCmd, v => v.btnSelectNextProfile).DisposeWith(disposables);
+33 -16
View File
@@ -66,6 +66,7 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@@ -232,15 +233,31 @@
Grid.Row="8" Grid.Row="8"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" 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" VerticalAlignment="Top"
Text="{x:Static resx:ResUI.LvRequestHeaders}" /> Text="{x:Static resx:ResUI.LvRequestHeaders}" />
<StackPanel Grid.Row="8" Grid.Column="1"> <StackPanel Grid.Row="9" Grid.Column="1">
<TextBox <TextBox
x:Name="txtRequestHeaders" x:Name="txtRequestHeaders"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
AcceptsReturn="True" AcceptsReturn="True"
MinLines="4"
MaxLines="8" MaxLines="8"
MinLines="4"
Style="{StaticResource MyOutlinedTextBox}" Style="{StaticResource MyOutlinedTextBox}"
TextWrapping="Wrap" TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" /> VerticalScrollBarVisibility="Auto" />
@@ -251,7 +268,7 @@
</StackPanel> </StackPanel>
<TextBlock <TextBlock
Grid.Row="9" Grid.Row="10"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -259,7 +276,7 @@
Text="{x:Static resx:ResUI.LvSort}" /> Text="{x:Static resx:ResUI.LvSort}" />
<TextBox <TextBox
x:Name="txtSort" x:Name="txtSort"
Grid.Row="9" Grid.Row="10"
Grid.Column="1" Grid.Column="1"
Width="100" Width="100"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
@@ -269,7 +286,7 @@
Style="{StaticResource MyOutlinedTextBox}" /> Style="{StaticResource MyOutlinedTextBox}" />
<TextBlock <TextBlock
Grid.Row="10" Grid.Row="11"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -277,7 +294,7 @@
Text="{x:Static resx:ResUI.LvPrevProfile}" /> Text="{x:Static resx:ResUI.LvPrevProfile}" />
<TextBox <TextBox
x:Name="txtPrevProfile" x:Name="txtPrevProfile"
Grid.Row="10" Grid.Row="11"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -287,7 +304,7 @@
ToolTip="{x:Static resx:ResUI.LvPrevProfileTip}" /> ToolTip="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button <Button
x:Name="btnSelectPrevProfile" x:Name="btnSelectPrevProfile"
Grid.Row="10" Grid.Row="11"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -295,7 +312,7 @@
Style="{StaticResource DefButton}" /> Style="{StaticResource DefButton}" />
<TextBlock <TextBlock
Grid.Row="11" Grid.Row="12"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -303,7 +320,7 @@
Text="{x:Static resx:ResUI.LvNextProfile}" /> Text="{x:Static resx:ResUI.LvNextProfile}" />
<TextBox <TextBox
x:Name="txtNextProfile" x:Name="txtNextProfile"
Grid.Row="11" Grid.Row="12"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -313,7 +330,7 @@
ToolTip="{x:Static resx:ResUI.LvPrevProfileTip}" /> ToolTip="{x:Static resx:ResUI.LvPrevProfileTip}" />
<Button <Button
x:Name="btnSelectNextProfile" x:Name="btnSelectNextProfile"
Grid.Row="11" Grid.Row="12"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -321,7 +338,7 @@
Style="{StaticResource DefButton}" /> Style="{StaticResource DefButton}" />
<TextBlock <TextBlock
Grid.Row="12" Grid.Row="13"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -329,14 +346,14 @@
Text="{x:Static resx:ResUI.LvCustomCoreType}" /> Text="{x:Static resx:ResUI.LvCustomCoreType}" />
<ComboBox <ComboBox
x:Name="cmbCustomCoreType" x:Name="cmbCustomCoreType"
Grid.Row="12" Grid.Row="13"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
MaxDropDownHeight="1000" MaxDropDownHeight="1000"
Style="{StaticResource MyOutlinedTextComboBox}" /> Style="{StaticResource MyOutlinedTextComboBox}" />
<TextBlock <TextBlock
Grid.Row="13" Grid.Row="14"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -344,7 +361,7 @@
Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" /> Text="{x:Static resx:ResUI.TbPreSocksPort4Sub}" />
<TextBox <TextBox
x:Name="txtPreSocksPort" x:Name="txtPreSocksPort"
Grid.Row="13" Grid.Row="14"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
@@ -354,7 +371,7 @@
ToolTip="{x:Static resx:ResUI.TipPreSocksPort}" /> ToolTip="{x:Static resx:ResUI.TipPreSocksPort}" />
<TextBlock <TextBlock
Grid.Row="14" Grid.Row="15"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -362,7 +379,7 @@
Text="{x:Static resx:ResUI.LvMemo}" /> Text="{x:Static resx:ResUI.LvMemo}" />
<TextBox <TextBox
x:Name="txtMemo" x:Name="txtMemo"
Grid.Row="14" Grid.Row="15"
Grid.Column="1" Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -9,6 +9,7 @@ public partial class SubEditWindow
Loaded += Window_Loaded; Loaded += Window_Loaded;
cmbConvertTarget.ItemsSource = Global.SubConvertTargets; cmbConvertTarget.ItemsSource = Global.SubConvertTargets;
cmbPreResolve.ItemsSource = Global.DomainDNS4PreResolveAddress;
cmbCustomCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList().AppendEmpty(); cmbCustomCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList().AppendEmpty();
this.WhenActivated(disposables => 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.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.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.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.SelectPrevProfileCmd, v => v.btnSelectPrevProfile).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SelectNextProfileCmd, v => v.btnSelectNextProfile).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SelectNextProfileCmd, v => v.btnSelectNextProfile).DisposeWith(disposables);