DnsClientForm/DnsClientForm.cs
2025-04-11 01:14:08 +08:00

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();
}
}
}