在实现一个简易的 DNS 查询客户端时,构造 DNS 报文是最关键的一步。DNS 报文大致由两个部分组成:
Header(报文头)
Question(问题)
本文聚焦于
dns_create_question
函数,即如何将用户输入的域名(如"www.example.com"
)编码为符合 DNS 协议格式的查询字段,并构造相关的qtype
与qclass
信息。
struct dns_question {
int length; // DNS问询字段总长度(包括name部分)
unsigned short qtype; // 查询类型,一般为A记录(0x0001)
unsigned short qclass; // 查询类,一般为IN(0x0001)
unsigned char *name; // 按DNS格式编码后的域名
};
DNS 协议中对域名的编码方式不同于普通字符串。例如:
输入字符串:www.example.com
DNS格式编码如下(十六进制):
03 77 77 77 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00
解释如下:
每个标签前添加一个长度字节(如 "www"
长度为 3)
标签本体("www", "example", "com")
最后以一个 0 字节结尾,表示域名结束
int dns_create_question(struct dns_question *question, const char *hostname) {
if (question == NULL || hostname == NULL) return -1;
memset(question, 0, sizeof(struct dns_question));
size_t hostlen = strlen(hostname);
question->name = (unsigned char*)malloc(hostlen + 2); // +2 是为了保险:包含结尾的0字节
if (question->name == NULL) return -2;
question->length = (int)hostlen + 2;
question->qtype = htons(1); // A记录,查询IPv4地址
question->qclass = htons(1); // IN类,代表Internet
const char delim[2] = ".";
unsigned char *qname = question->name;
// strdup复制hostname,避免strtok破坏原始字符串
char *hostname_dup = strdup(hostname);
if (hostname_dup == NULL) {
free(question->name);
return -3;
}
char *token = strtok(hostname_dup, delim);
while (token != NULL) {
size_t len = strlen(token);
*qname = (unsigned char)len; // 写入长度前缀
qname++;
memcpy(qname, token, len); // 拷贝标签内容
qname += len;
token = strtok(NULL, delim);
}
*qname = 0; // DNS要求以0x00结尾标志域名结束
free(hostname_dup);
return 0;
}
question->name = (unsigned char*)malloc(hostlen + 2);
这里 +2
是为保证:
结尾的 0
字节(必须)
至少一个 .
的情况下也有余量
DNS 协议中使用“长度 + 标签”的格式编码域名:
例如 www.example.com
变成:
[3] w w w [7] e x a m p l e [3] c o m [0]
每一段前都要加一个长度字节,结尾加 0x00
表示结束。这是 DNS 报文格式的核心要求。
question->qtype = htons(1);
question->qclass = htons(1);
htons()
是“host to network short”的缩写,把主机字节序(Little Endian)转为网络字节序(Big Endian)。
qtype = 1
表示 A记录查询(IPv4 地址)
qclass = 1
表示 IN类,即 Internet 网络查询
这些值在大多数实际 DNS 查询中是默认的标准设置。
const char delim[2] = ".";
unsigned char *qname = question->name;
char *hostname_dup = strdup(hostname);
if (hostname_dup == NULL) {
free(question->name);
return -3;
}
分隔符设置:DNS 域名由 .
分隔,如 www.example.com
分成三段。
复制 hostname:
因为 strtok()
函数会修改原字符串(它会把 .
替换成 \0
),所以使用 strdup()
创建一份副本。
异常处理:
如果 strdup
失败,释放之前已分配的 name
内存,并返回 -3
,防止内存泄漏。
char *token = strtok(hostname_dup, delim);
while (token != NULL) {
size_t len = strlen(token);
*qname = (unsigned char)len; // 写入长度前缀
qname++;
memcpy(qname, token, len); // 拷贝标签内容
qname += len;
token = strtok(NULL, delim);
}
这是整段函数中最核心的部分 —— 将普通域名字符串转换为 DNS 报文格式的 QNAME:
举个例子,www.example.com
会被编码成如下形式:
03 77 77 77 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00
*qname = len
:每段前加长度字节。
memcpy(qname, token, len)
:将每段标签拷贝到缓冲区。
qname += len
:更新写入指针。
循环直到没有更多 label。
https://github.com/0voice