421 lines
15 KiB
C#
421 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Collections.Concurrent;
|
|
using System.Threading;
|
|
using System.Text.Json;
|
|
using System.Net.Security;
|
|
using System.Net.Sockets;
|
|
using DnsClientX;
|
|
|
|
namespace DnsClient
|
|
{
|
|
public partial class DnsClientForm : Form
|
|
{
|
|
public DnsClientForm()
|
|
{
|
|
InitializeComponent();
|
|
}
|
|
|
|
private (string result, int time) fastestDohResult;
|
|
private (string result, int time) fastestDotResult;
|
|
|
|
public async Task<(List<string> dohResults, List<string> dotResults)> GetNotice(string domain)
|
|
{
|
|
fastestDohResult = (null, int.MaxValue);
|
|
fastestDotResult = (null, int.MaxValue);
|
|
|
|
// 创建DoH客户端列表
|
|
var dohServers = new List<string>
|
|
{
|
|
"https://cloudflare-dns.com/dns-query",
|
|
"https://dns.cloudflare.com/dns-query",
|
|
"https://1.1.1.1/dns-query",
|
|
"https://1.0.0.1/dns-query",
|
|
"https://dns.google/resolve",
|
|
"https://sm2.doh.pub/dns-query",
|
|
"https://doh.pub/dns-query",
|
|
"https://dns.alidns.com/resolve",
|
|
"https://223.5.5.5/resolve",
|
|
"https://223.6.6.6/resolve",
|
|
"https://doh.360.cn/resolve"
|
|
};
|
|
|
|
// 创建DoT客户端列表
|
|
var dotServers = new List<NameServer>
|
|
{
|
|
// 360
|
|
new NameServer(IPAddress.Parse("101.226.4.6"), 853),
|
|
// Aliyun
|
|
new NameServer(IPAddress.Parse("223.5.5.5"), 853),
|
|
new NameServer(IPAddress.Parse("223.6.6.6"), 853),
|
|
// Tencent
|
|
new NameServer(IPAddress.Parse("1.12.12.12"), 853),
|
|
// Cloudflare DoT
|
|
new NameServer(IPAddress.Parse("1.1.1.1"), 853),
|
|
new NameServer(IPAddress.Parse("1.0.0.1"), 853),
|
|
// Google DoT
|
|
new NameServer(IPAddress.Parse("8.8.8.8"), 853),
|
|
new NameServer(IPAddress.Parse("8.8.4.4"), 853),
|
|
// Quad9 DoT
|
|
new NameServer(IPAddress.Parse("9.9.9.9"), 853),
|
|
new NameServer(IPAddress.Parse("149.112.112.112"), 853)
|
|
};
|
|
|
|
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
|
|
var dohResults = new ConcurrentBag<string>();
|
|
var dotResults = new ConcurrentBag<string>();
|
|
|
|
var dohTasks = dohServers.Select(server =>
|
|
QueryDohAsync(server, domain, dohResults, cts.Token)).ToList();
|
|
var dotTasks = dotServers.Select(server =>
|
|
QueryDotAsync(server, domain, dotResults, cts.Token)).ToList();
|
|
|
|
await Task.WhenAll(dohTasks.Concat(dotTasks));
|
|
|
|
return (dohResults.ToList(), dotResults.ToList());
|
|
|
|
}
|
|
|
|
private async Task QueryDohAsync(string server, string domain, ConcurrentBag<string> results, CancellationToken token)
|
|
{
|
|
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
|
try
|
|
{
|
|
var httpClient = new HttpClient();
|
|
httpClient.Timeout = TimeSpan.FromSeconds(5);
|
|
httpClient.DefaultRequestHeaders.Add("Accept", "application/dns-json");
|
|
// Change type=1 (A record) to type=16 (TXT record)
|
|
var response = await httpClient.GetAsync($"{server}?name={domain}&type=16", token);
|
|
response.EnsureSuccessStatusCode();
|
|
var responseString = await response.Content.ReadAsStringAsync();
|
|
var json = JsonDocument.Parse(responseString);
|
|
|
|
stopwatch.Stop();
|
|
var elapsedMs = (int)stopwatch.ElapsedMilliseconds;
|
|
|
|
if (json.RootElement.TryGetProperty("Answer", out var answers))
|
|
{
|
|
foreach (var answer in answers.EnumerateArray())
|
|
{
|
|
// For TXT records, the data might be in quotes, so we'll just display it as-is
|
|
string txtData = answer.GetProperty("data").GetString();
|
|
string result = $"{server} => {txtData} (耗时: {elapsedMs}ms)";
|
|
results.Add(result);
|
|
|
|
if (elapsedMs < fastestDohResult.time)
|
|
{
|
|
fastestDohResult = (server, elapsedMs);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string result = $"{server} => 无 TXT 记录 (耗时: {elapsedMs}ms)";
|
|
results.Add(result);
|
|
|
|
if (fastestDohResult.result == null || elapsedMs < fastestDohResult.time)
|
|
{
|
|
fastestDohResult = (result, elapsedMs);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
stopwatch.Stop();
|
|
var elapsedMs = (int)stopwatch.ElapsedMilliseconds;
|
|
string result = $"{server} => 错误: {ex.Message} (耗时: {elapsedMs}ms)";
|
|
results.Add(result);
|
|
|
|
if (fastestDohResult.result == null)
|
|
{
|
|
fastestDohResult = (result, elapsedMs);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task QueryDotAsync(NameServer server, string domain, ConcurrentBag<string> results, CancellationToken token)
|
|
{
|
|
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
|
try
|
|
{
|
|
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(token))
|
|
{
|
|
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
|
|
|
using (var tcpClient = new TcpClient())
|
|
{
|
|
var connectTask = tcpClient.ConnectAsync(server.Address.ToString(), server.Port);
|
|
var timeoutTask = Task.Delay(5000, cts.Token);
|
|
|
|
if (await Task.WhenAny(connectTask, timeoutTask) == timeoutTask)
|
|
{
|
|
throw new TimeoutException("连接超时");
|
|
}
|
|
|
|
using (var sslStream = new SslStream(tcpClient.GetStream(), false,
|
|
(sender, certificate, chain, sslPolicyErrors) => true))
|
|
{
|
|
var authTask = sslStream.AuthenticateAsClientAsync(server.Address.ToString());
|
|
if (await Task.WhenAny(authTask, timeoutTask) == timeoutTask)
|
|
{
|
|
throw new TimeoutException("SSL握手超时");
|
|
}
|
|
|
|
var request = CreateDnsQuery(domain);
|
|
|
|
var writeTask = sslStream.WriteAsync(request, 0, request.Length, cts.Token);
|
|
if (await Task.WhenAny(writeTask, timeoutTask) == timeoutTask)
|
|
{
|
|
throw new TimeoutException("写入请求超时");
|
|
}
|
|
|
|
var response = new byte[512];
|
|
|
|
var readTask = sslStream.ReadAsync(response, 0, response.Length, cts.Token);
|
|
if (await Task.WhenAny(readTask, timeoutTask) == timeoutTask)
|
|
{
|
|
throw new TimeoutException("读取响应超时");
|
|
}
|
|
|
|
int bytesRead = await readTask;
|
|
stopwatch.Stop();
|
|
var elapsedMs = (int)stopwatch.ElapsedMilliseconds;
|
|
|
|
var txtRecords = ParseDnsResponse(response, bytesRead);
|
|
string serverInfo = $"{server.Address}:{server.Port}";
|
|
|
|
if (txtRecords.Any())
|
|
{
|
|
foreach (var txt in txtRecords)
|
|
{
|
|
string result = $"{serverInfo} => {txt} (耗时: {elapsedMs}ms)";
|
|
results.Add(result);
|
|
|
|
if (elapsedMs < fastestDotResult.time)
|
|
{
|
|
fastestDotResult = (serverInfo, elapsedMs);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string result = $"{serverInfo} => 无 TXT 记录 (耗时: {elapsedMs}ms)";
|
|
results.Add(result);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
stopwatch.Stop();
|
|
var elapsedMs = (int)stopwatch.ElapsedMilliseconds;
|
|
string serverInfo = $"{server.Address}:{server.Port}";
|
|
string result = $"{serverInfo} => 错误: {ex.Message} (耗时: {elapsedMs}ms)";
|
|
results.Add(result);
|
|
}
|
|
}
|
|
|
|
private byte[] CreateDnsQuery(string domain)
|
|
{
|
|
var random = new Random();
|
|
ushort transactionId = (ushort)random.Next(0, ushort.MaxValue);
|
|
|
|
var query = new List<byte>();
|
|
|
|
// Transaction ID
|
|
query.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)transactionId)));
|
|
|
|
// Flags: standard query (0x0100)
|
|
query.Add(0x01);
|
|
query.Add(0x00);
|
|
|
|
// Questions: 1
|
|
query.Add(0x00);
|
|
query.Add(0x01);
|
|
|
|
// Answer RRs: 0
|
|
query.Add(0x00);
|
|
query.Add(0x00);
|
|
|
|
// Authority RRs: 0
|
|
query.Add(0x00);
|
|
query.Add(0x00);
|
|
|
|
// Additional RRs: 0
|
|
query.Add(0x00);
|
|
query.Add(0x00);
|
|
|
|
// Question section
|
|
var labels = domain.Split('.');
|
|
foreach (var label in labels)
|
|
{
|
|
query.Add((byte)label.Length);
|
|
query.AddRange(System.Text.Encoding.ASCII.GetBytes(label));
|
|
}
|
|
query.Add(0x00); // End of domain name
|
|
|
|
// Type TXT (0x0010) instead of A (0x0001)
|
|
query.Add(0x00);
|
|
query.Add(0x10);
|
|
|
|
// Class IN (0x0001)
|
|
query.Add(0x00);
|
|
query.Add(0x01);
|
|
|
|
// Prefix length for TLS framing
|
|
var length = query.Count;
|
|
var framed = new List<byte>
|
|
{
|
|
(byte)(length >> 8),
|
|
(byte)(length & 0xFF)
|
|
};
|
|
framed.AddRange(query);
|
|
|
|
return framed.ToArray();
|
|
}
|
|
|
|
|
|
private List<string> ParseDnsResponse(byte[] response, int length)
|
|
{
|
|
var txtRecords = new List<string>();
|
|
|
|
// Skip TLS length prefix (2 bytes)
|
|
int offset = 2;
|
|
|
|
// Transaction ID (2 bytes) + Flags (2 bytes)
|
|
offset += 4;
|
|
|
|
// Questions
|
|
int qdCount = (response[offset] << 8) | response[offset + 1];
|
|
offset += 2;
|
|
|
|
// Answer RRs
|
|
int anCount = (response[offset] << 8) | response[offset + 1];
|
|
offset += 6; // Skip authority + additional too
|
|
|
|
// Skip question section
|
|
for (int i = 0; i < qdCount; i++)
|
|
{
|
|
while (response[offset] != 0)
|
|
{
|
|
offset += response[offset] + 1;
|
|
}
|
|
offset += 5; // null byte + QTYPE(2) + QCLASS(2)
|
|
}
|
|
|
|
// Parse answer section
|
|
for (int i = 0; i < anCount; i++)
|
|
{
|
|
// Skip name (compressed)
|
|
offset += 2;
|
|
|
|
ushort type = (ushort)((response[offset] << 8) | response[offset + 1]);
|
|
offset += 8; // TYPE(2) + CLASS(2) + TTL(4)
|
|
|
|
ushort dataLength = (ushort)((response[offset] << 8) | response[offset + 1]);
|
|
offset += 2;
|
|
|
|
if (type == 16) // TXT record
|
|
{
|
|
int end = offset + dataLength;
|
|
var txtBuilder = new List<string>();
|
|
|
|
while (offset < end)
|
|
{
|
|
int txtLen = response[offset++];
|
|
if (offset + txtLen > end) break;
|
|
|
|
var txt = System.Text.Encoding.UTF8.GetString(response, offset, txtLen);
|
|
txtBuilder.Add(txt);
|
|
offset += txtLen;
|
|
}
|
|
|
|
txtRecords.Add(string.Join("", txtBuilder));
|
|
}
|
|
else
|
|
{
|
|
offset += dataLength;
|
|
}
|
|
}
|
|
|
|
return txtRecords;
|
|
}
|
|
|
|
private string GetPreferredResult()
|
|
{
|
|
// 只返回成功的解析结果
|
|
string preferredDoh = fastestDohResult.time != int.MaxValue ?
|
|
fastestDohResult.result : "无可用DoH结果";
|
|
|
|
string preferredDot = fastestDotResult.time != int.MaxValue ?
|
|
fastestDotResult.result : "无可用DoT结果";
|
|
|
|
return $"=== 优选结果 ===\n" +
|
|
$"DoH: {preferredDoh}\n" +
|
|
$"DoT: {preferredDot}";
|
|
}
|
|
|
|
// 辅助类型定义
|
|
private enum ResultType { Success, NoRecord, Error }
|
|
|
|
private class DnsResult
|
|
{
|
|
public ResultType Type { get; set; }
|
|
public string FullResult { get; set; }
|
|
public string IP { get; set; }
|
|
public int Time { get; set; }
|
|
}
|
|
|
|
private async void Start_Button_Click(object sender, EventArgs e)
|
|
{
|
|
Select_Text.Clear();
|
|
Result_Text.Clear();
|
|
string domain = Domain_Text.Text.Trim();
|
|
|
|
if (string.IsNullOrEmpty(domain))
|
|
{
|
|
Select_Text.AppendText("请输出正确的域名!\n");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
Select_Text.AppendText($"正在查询: {domain}\n");
|
|
var (dohResults, dotResults) = await GetNotice(domain);
|
|
|
|
Select_Text.AppendText("=== DoH 查询结果 ===\n");
|
|
foreach (var result in dohResults.OrderBy(r => r))
|
|
{
|
|
Select_Text.AppendText(result + "\n");
|
|
}
|
|
|
|
Select_Text.AppendText("=== DoT 查询结果 ===\n");
|
|
foreach (var result in dotResults.OrderBy(r => r))
|
|
{
|
|
Select_Text.AppendText(result + "\n");
|
|
}
|
|
|
|
Result_Text.AppendText(GetPreferredResult());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Select_Text.AppendText($"查询出错: {ex.Message}\n");
|
|
}
|
|
}
|
|
|
|
private void Exit_Button_Click(object sender, EventArgs e)
|
|
{
|
|
this.Close();
|
|
Application.Exit();
|
|
}
|
|
}
|
|
}
|