fnettrace_dns.h
文章目录
- fnettrace_dns.h
- main.c
- print_dns
- custom_bpf
- print_date
- run_trace
- main
- 补充DNS
- DNS协议报文格式
- **问题记录**
- **资源记录**
- **报文实例**
- **请求报文**
- **响应报文**
- DNS解析过程
- DNS 出现
- DNS 介绍
- DNS 域名
- DNS 域名组成
- DNS 域名空间
- DNS 域名解析
- DNS 解析器
- DNS 域名服务器
- DNS 域名服务器类型
- DNS 域名完整解析过程
- DNS 传输层协议
- DNS 查询方式
- 递归查询
- 迭代查询
- DNS 反向查询
- DNS 应用场景
- 静态域名解析
- 动态域名解析
- DNS 代理
- 动态域名解析
- DNS 代理
#ifndef FNETTRACE_DNS_H
#define FNETTRACE_DNS_H#include "../include/common.h"
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <stdarg.h>
#include <fcntl.h>
#include <sys/mman.h>#endif
main.c
#include "fnettrace_dns.h"
#include <sys/ioctl.h>
#include <time.h>
#include <linux/filter.h>
#include <linux/if_ether.h>
#include <sys/prctl.h>
#include <signal.h>
#define MAX_BUF_SIZE (64 * 1024)static int arg_nolocal = 0;
static char last[512] = {'\0'};// pkt - start of DNS layer
void print_dns(uint32_t ip_src, unsigned char *pkt) {assert(pkt);char ip[30];sprintf(ip, "%d.%d.%d.%d", PRINT_IP(ip_src));time_t seconds = time(NULL);struct tm *t = localtime(&seconds);int nxdomain = ((*(pkt + 3) & 0x03) == 0x03)? 1: 0;// expecting a single question countif (pkt[4] != 0 || pkt[5] != 1)goto errout;// check cnameunsigned char *ptr = pkt + 12;int len = 0;while (*ptr != 0 && len < 255) { // 255 is the maximum length of a domain name including multiple '.'if (*ptr > 63) // the name left of a '.' is 63 length maximumgoto errout;int delta = *ptr + 1;*ptr = '.';len += delta;;ptr += delta;}if (*ptr != 0)goto errout;ptr++;uint16_t type;memcpy(&type, ptr, 2);type = ntohs(type);// filter outputchar tmp[sizeof(last)];snprintf(tmp, sizeof(last), "%02d:%02d:%02d %-15s DNS %s (type %u)%s",t->tm_hour, t->tm_min, t->tm_sec, ip, pkt + 12 + 1,type, (nxdomain)? " NXDOMAIN": "");if (strcmp(tmp, last)) {printf("%s\n", tmp);fflush(0);strcpy(last, tmp);}return;errout:printf("%02d:%02d:%02d %15s Error: invalid DNS packet\n", t->tm_hour, t->tm_min, t->tm_sec, ip);fflush(0);
}// https://www.kernel.org/doc/html/latest/networking/filter.html
static void custom_bpf(int sock) {struct sock_filter code[] = {// sudo tcpdump ip and udp and src port 53 -dd{ 0x28, 0, 0, 0x0000000c },{ 0x15, 0, 8, 0x00000800 },{ 0x30, 0, 0, 0x00000017 },{ 0x15, 0, 6, 0x00000011 },{ 0x28, 0, 0, 0x00000014 },{ 0x45, 4, 0, 0x00001fff },{ 0xb1, 0, 0, 0x0000000e },{ 0x48, 0, 0, 0x0000000e },{ 0x15, 0, 1, 0x00000035 },{ 0x6, 0, 0, 0x00040000 },{ 0x6, 0, 0, 0x00000000 },};struct sock_fprog bpf = {.len = (unsigned short) sizeof(code) / sizeof(code[0]),.filter = code,};int rv = setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));if (rv < 0) {fprintf(stderr, "Error: cannot attach BPF filter\n");exit(1);}
}static void print_date(void) {static int day = -1;time_t now = time(NULL);struct tm *t = localtime(&now);if (day != t->tm_yday) {printf("DNS trace for %s", ctime(&now));day = t->tm_yday;}fflush(0);
}static void run_trace(void) {// grab all Ethernet packets and use a custom BPF filter to get only UDP from source port 53int s = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));if (s < 0)errExit("socket");custom_bpf(s);struct timeval tv;tv.tv_sec = 10;tv.tv_usec = 0;unsigned char buf[MAX_BUF_SIZE];while (1) {fd_set rfds;FD_ZERO(&rfds);FD_SET(s, &rfds);int rv = select(s + 1, &rfds, NULL, NULL, &tv);if (rv < 0)errExit("select");else if (rv == 0) {print_date();tv.tv_sec = 10;tv.tv_usec = 0;continue;}unsigned bytes = recvfrom(s, buf, MAX_BUF_SIZE, 0, NULL, NULL);if (bytes >= (14 + 20 + 8)) { // size of MAC + IP + UDP headersuint8_t ip_hlen = (buf[14] & 0x0f) * 4;uint16_t port_src;memcpy(&port_src, buf + 14 + ip_hlen, 2);port_src = ntohs(port_src);uint8_t protocol = buf[14 + 9];uint32_t ip_src;memcpy(&ip_src, buf + 14 + 12, 4);ip_src = ntohl(ip_src);if (arg_nolocal) {if ((ip_src & 0xff000000) == 0x7f000000 || // 127.0.0.0/8(ip_src & 0xff000000) == 0x0a000000 || // 10.0.0.0/8(ip_src & 0xffff0000) == 0xc0a80000 || // 192.168.0.0/16(ip_src & 0xfff00000) == 0xac100000) // 172.16.0.0/12continue;}// if DNS packet, extract the queryif (port_src == 53 && protocol == 0x11) // UDP protocolprint_dns(ip_src, buf + 14 + ip_hlen + 8); // IP and UDP header len}}close(s);
}
static const char *const usage_str ="Usage: fnettrace-dns [OPTIONS]\n""Options:\n"" --help, -? - this help screen\n"" --nolocal\n";static void usage(void) {puts(usage_str);
}int main(int argc, char **argv) {int i;for (i = 1; i < argc; i++) {if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-?") == 0) {usage();return 0;}else if (strcmp(argv[i], "--nolocal") == 0)arg_nolocal = 1;else {fprintf(stderr, "Error: invalid argument\n");return 1;}}if (getuid() != 0) {fprintf(stderr, "Error: you need to be root to run this program\n");return 1;}// kill the process if the parent diedprctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0);print_date();run_trace();return 0;
}
这个程序文件是一个用于追踪DNS查询的工具。它使用BPF(Berkeley Packet Filter)过滤器从网络数据包中提取DNS查询,并输出到控制台。以下是对该程序功能的分析:
-
print_dns
函数:解析并打印DNS查询。它接收源IP地址和指向DNS层的指针作为参数。首先,将源IP地址转换为字符串格式。然后检查DNS查询是否有效,包括域名长度、类型等。最后,根据查询是否为NXDOMAIN(Non-Existent Domain)输出相应的信息。 -
custom_bpf
函数:设置自定义BPF过滤器。此过滤器仅捕获来自源端口53的UDP数据包。 -
print_date
函数:在每十分钟更新一次日期。 -
run_trace
函数:主循环,用于读取和处理数据包。首先创建一个原始套接字以捕获所有类型的以太网数据包,然后应用自定义的BPF过滤器。接着进入一个无限循环,在每次循环中调用select
来等待新的数据包到达。当有新数据包时,调用recvfrom
接收数据包。如果数据包包含DNS查询,则调用print_dns
进行解析和打印。 -
main
函数:程序的入口点。在此函数中,处理命令行选项,检查当前用户是否具有root权限,然后调用print_date
和run_trace
开始追踪DNS查询。
总之,这个程序文件实现了一个简单的DNS查询追踪工具,通过捕获网络数据包中的DNS查询并进行解析,将其结果显示在控制台上。
print_dns
这段代码是用来解析DNS数据包并打印相关信息的函数。以下是对每一行代码的详细解释:
-
static int arg_nolocal = 0;
:定义一个静态整型变量arg_nolocal,并初始化为0。 -
static char last[512] = {'\0'};
:定义一个静态字符数组last,长度为512,所有元素初始化为空字符’\0’。 -
void print_dns(uint32_t ip_src, unsigned char *pkt)
:定义一个名为print_dns的函数,接受两个参数,一个无符号32位整型变量ip_src(表示IP源地址)和一个无符号字符指针pkt(指向DNS层的起始位置)。 -
assert(pkt);
:检查pkt是否为空指针,如果为空则程序会终止运行并输出错误信息。 -
char ip[30];
:定义一个字符数组ip,长度为30,用于存储IP地址的字符串形式。 -
sprintf(ip, "%d.%d.%d.%d", PRINT_IP(ip_src));
:使用sprintf函数将ip_src转换为点分十进制的IP地址格式,并存入ip数组中。
// macro to print ip addresses in a printf statement #define PRINT_IP(A) \ ((int) (((A) >> 24) & 0xFF)), ((int) (((A) >> 16) & 0xFF)), ((int) (((A) >> 8) & 0xFF)), ((int) ( (A) & 0xFF))
定义在common.h中
- 宏
PRINT_IP(A)
: 这个宏用于在printf语句中打印IPv4地址。它接受一个32位的无符号整数A(代表IPv4地址),并通过位移和按位与操作将其拆分为四个8位的字节,并将它们转换为int类型后返回。这四个字节按照IPv4地址的格式(从高字节到低字节)排列。这个宏
PRINT_IP(A)
的目的是将一个32位的无符号整数(代表IPv4地址)转换为四个8位的整数,并按照IPv4地址的标准格式(从高字节到低字节)排列。以下是对代码的详细解释:
(A) >> 24
:这是右移运算符,将输入的32位整数A向右移动24位。由于是无符号整数,高位补0。这样可以得到最高字节(第四个字节)的内容。
& 0xFF
:这是按位与运算符,将上一步的结果与0xFF进行按位与操作。0xFF是一个8位的二进制数,每一位都是1,这相当于对结果的低8位进行保留,高24位清零。这样就可以确保得到的是一个8位的整数。将上述表达式用
(int)
强制转换为 int 类型,确保在printf语句中正确输出。接下来的三个表达式
(A) >> 16
,(A) >> 8
,(A) & 0xFF
分别用于获取IP地址的第三、第二和第一个字节。它们的处理方式与获取第四个字节的方式类似,只是右移的位数不同。最后,这些表达式通过逗号运算符
,
连接在一起。在C语言中,逗号运算符会计算所有操作数并返回最后一个操作数的值。但在这个宏中,我们并不关心返回的值,而是利用逗号运算符的副作用——顺序执行各个表达式并将结果作为参数传递给printf函数。因此,当你在printf语句中使用
PRINT_IP(some_ip_address)
时,它会被展开为四个8位的整数,按照IPv4地址的标准格式排列,可以直接用于打印IP地址。例如:uint32_t ip = 0x01020304; printf("IP address: %d.%d.%d.%d\n", PRINT_IP(ip));
输出将是:
IP address: 1.2.3.4
-
time_t seconds = time(NULL);
:获取当前时间(以秒为单位),并存储在seconds变量中。 -
struct tm *t = localtime(&seconds);
:将seconds转换为本地时间,并存储在一个tm结构体指针t中。 -
int nxdomain = ((*(pkt + 3) & 0x03) == 0x03)? 1: 0;
:根据DNS数据包中的某个标志位判断是否为NXDOMAIN(不存在的域名)查询,如果是则nxdomain为1,否则为0。
这段代码是在处理网络数据包(
pkt
)时,检查其中的某个字段以确定是否为NXDOMAIN(Non-Existent Domain,不存在的域名)响应。首先,它通过指针运算
*(pkt + 3)
访问数据包中的第4个字节(因为C语言数组下标从0开始计数)。然后,使用按位与运算符
&
将该字节与常量0x03
进行按位与操作,这样可以提取出该字节的最低两位。接着,使用条件运算符
? :
来判断提取出的最低两位是否等于0x03
:
- 如果等于
0x03
,则条件为真,表达式的结果为1
,表示这是一个NXDOMAIN响应。- 如果不等于
0x03
,则条件为假,表达式的结果为0
,表示这不是一个NXDOMAIN响应。最终,这个表达式的值
nxdomain
是一个整数,用于表示当前数据包是否为NXDOMAIN响应。如果nxdomain
等于1
,则表示是NXDOMAIN响应;如果nxdomain
等于0
,则表示不是NXDOMAIN响应。==================================================================================
使用按位与运算符
&
将一个字节与常量0x03
进行操作,可以提取出该字节的最低两位,这是因为0x03
的二进制表示为00000011
。在按位与运算中,当两个对应的二进制位都为1时,结果位才为1,否则为0。因此,将任意一个字节与
0x03
进行按位与操作时,只有该字节的最低两位与0x03
的最低两位(都是1)进行计算,其余高位由于与0x03
的对应位均为0,所以结果均为0。例如,假设我们要提取的字节为
0b10101010
(即十进制的170),与0x03
(即二进制的0b00000011
)进行按位与操作:0b10101010 (原始字节) & 0b00000011 (常量0x03) -------------------0b00000010 (结果)
可以看到,结果中的高位都被清零,只剩下原始字节的最低两位。因此,通过这种方式,我们可以提取出一个字节的最低两位。在这个特定的上下文中,这可能用于检查数据包中的特定标志或编码信息。
================================================================================
取出最低两位的具体原因取决于上下文和协议的定义。在不同的网络协议或数据格式中,最低两位可能包含特定的标志、状态信息或其他编码数据。
在这个例子中,检查最低两位是否等于
0x03
可能是为了识别某种协议的特定响应类型或者错误代码。例如,在DNS(Domain Name System)协议中,某些类型的响应可能会在消息中的特定位置设置特定的位模式来表示不同的状态,如NXDOMAIN(Non-Existent Domain)。具体来说,如果协议规定了NXDOMAIN响应时某个字节的最低两位应为
0b11
(即十进制的3,十六进制的0x03),那么通过按位与运算提取出最低两位并检查是否等于0x03
,就可以确定这个响应是否为NXDOMAIN。因此,取出最低两位是为了提取和分析协议中特定的位信息,这些信息对于理解数据包的内容和含义至关重要。具体的含义和用法需要参考相关协议或数据格式的规范文档。
=====================================================================================
在
int nxdomain = ((*(pkt + 3) & 0x03) == 0x03)? 1: 0;
这一行代码中,*(pkt + 3)
是用来访问数据包(pkt
)中的特定字节。网络数据包通常由多个字节组成,这些字节包含了各种协议字段和数据。在这个上下文中,
pkt
是指向数据包起始位置的指针。由于C语言中的数组下标是从0开始的,所以
*(pkt + 3)
实际上是访问数据包中的第4个字节(因为3 + 0 = 3)。选择访问第4个字节的原因可能与特定的网络协议或数据格式有关。在这个例子中,可能是DNS(Domain Name System)协议或其他相关协议规定了NXDOMAIN(Non-Existent Domain)响应的某个标志或状态信息存储在数据包的第4个字节中的特定位。通过
*(pkt + 3)
访问这个字节,并使用按位与运算符&
与常量0x03
进行操作,可以提取出该字节中的特定位(通常是最低两位),然后进一步检查这些位是否符合NXDOMAIN响应的特征(在这种情况下,是否等于0x03
)。根据检查结果,将nxdomain
设置为1表示这是一个NXDOMAIN响应,否则设置为0表示不是NXDOMAIN响应。
if (pkt[4] != 0 || pkt[5] != 1)
:检查DNS数据包中的问题计数是否为1,如果不是则跳转到errout标签。
在DNS数据包中,特定字段的位置是根据DNS协议的定义来确定的。通常,DNS数据包的结构如下:
- DNS报文头部(12字节)
- ID(Identification,2字节)
- 标志(Flags,2字节)
- 问题计数(Question Count,2字节)
- 解答计数(Answer Count,2字节)
- 权威记录计数(Authority Count,2字节)
- 额外记录计数(Additional Count,2字节)
if (pkt[4] != 0 || pkt[5] != 1)
这一行代码是在检查DNS数据包中的问题计数(Question Count)字段。由于问题计数字段位于DNS报文头部的第4和第5个字节(从第0个字节开始计数),所以使用下标为4和下标为5来访问这两个字节。
问题计数是一个16位的无符号整数,其中低字节存储在第4个字节(pkt[4]),高字节存储在第5个字节(pkt[5])。在这个情况下,代码只检查了问题计数是否不为1,也就是说,它期望问题计数为1。
如果
(pkt[4] != 0 || pkt[5] != 1)
条件为真,即问题计数不是1(注意,这里假设pkt[4]应该是0,pkt[5]应该是1),那么程序会跳转到标记为errout
的位置进行错误处理。这是因为问题计数为1通常表示一个标准的DNS查询请求,如果不是1,则可能表示数据包格式有误或者是一个非标准的DNS操作。
-
unsigned char *ptr = pkt + 12;
:定义一个无符号字符指针ptr,指向DNS数据包中域名开始的位置。 -
int len = 0;
:定义一个整型变量len,用于记录域名的长度。 -
while (*ptr != 0 && len < 255) { ... }
:当ptr指向的字符不为0且len小于255时,循环处理域名。 -
if (*ptr > 63) { ... }
:检查域名中的每一个标签(即点分隔的部分)长度是否超过63,如果超过则跳转到errout标签。 -
int delta = *ptr + 1;
:计算当前标签的长度(包括标签本身和跟随的null字符)。 -
*ptr = '.';
:将当前标签的首字符替换为点号。 -
len += delta;
:增加len的值,表示域名长度增加了delta。 -
ptr += delta;
:将ptr向前移动delta个字符,指向下一个标签。 -
if (*ptr != 0) { ... }
:检查域名末尾是否有一个null字符,如果没有则跳转到errout标签。 -
ptr++;
:将ptr向前移动一个字符,指向类型字段。 -
uint16_t type;
:定义一个无符号16位整型变量type,用于存储DNS查询类型。 -
memcpy(&type, ptr, 2);
:从ptr指向的位置复制2个字节的数据到type变量中。 -
type = ntohs(type);
:将type变量中的网络字节序转换为主机字节序。 -
char tmp[sizeof(last)];
:定义一个临时字符数组tmp,长度与last数组相同。 -
snprintf(tmp, sizeof(last), "%02d:%02d:%02d %-15s DNS %s (type %u)%s", ...);
:使用snprintf函数将时间、IP地址、域名、类型和NXDOMAIN标记(如果适用)格式化为一个字符串,并存入tmp数组中。 -
if (strcmp(tmp, last)) { ... }
:比较tmp和last数组的内容,如果不同则执行以下操作。 -
printf("%s\n", tmp);
:输出tmp数组中的格式化字符串。 -
fflush(0);
:刷新标准输出流。 -
strcpy(last, tmp);
:将tmp数组的内容复制到last数组中。 -
return;
:结束函数。 -
errout:
:标签,用于处理错误情况。 -
printf("%02d:%02d:%02d %15s Error: invalid DNS packet\n", t->tm_hour, t->tm_min, t->tm_sec, ip);
:输出错误信息,包括时间、IP地址和错误描述。 -
fflush(0);
:刷新标准输出流。
该函数的主要功能是解析DNS数据包,提取出其中的时间、IP地址、域名和查询类型等信息,并根据需要输出到屏幕上。同时,它还对域名的格式进行了检查,确保其符合DNS协议的规定。如果数据包无效或不符合要求,则输出错误信息。
custom_bpf
这段代码定义了一个名为custom_bpf
的函数,其功能是为给定的套接字(socket)设置一个自定义的Berkeley Packet Filter(BPF)。这个BPF过滤器的作用是筛选出符合特定条件的IPv4 UDP数据包,具体来说,源端口为53的数据包(通常用于DNS查询)。
以下是对每一行代码的详细解释:
-
static void custom_bpf(int sock) {
:定义一个静态函数custom_bpf
,接受一个整型参数sock
(表示要设置BPF过滤器的套接字)。 -
struct sock_filter code[] = { ... };
:定义一个sock_filter
结构体数组code
,其中包含了BPF过滤器的指令集。这些指令用于检查和过滤网络数据包。
sock_filter
是 Linux 内核中伯克利数据包过滤器 (BPF) API 的一个结构。它用于定义网络数据包的个别过滤指令。
sock_filter
结构通常包含以下字段:struct sock_filter {__u16 code; // 过滤指令的操作码__u32 jt; // 条件为真时的跳转目标(相对偏移)__u32 jf; // 条件为假时的跳转目标(相对偏移)__u32 k; // 用于指令的操作数 };
以下是对每个字段的简要解释:
code
: 此字段表示过滤指令的操作码或opcode。它指定要在数据包数据上执行的操作类型。
jt
和jf
: 这些字段用于过滤程序中的条件跳转。jt
指定了如果条件为真时要跳转到的相对偏移,而jf
指定了如果条件为假时要跳转到的相对偏移。
k
: 此字段是某些过滤指令使用的操作数。其含义取决于正在使用的特定opcode。一组
sock_filter
结构组合成一个 BPF 程序,该程序可以附加到套接字上来根据某些标准过滤入站或出站网络数据包。BPF 虚拟机为每个数据包执行此程序,并根据程序执行结果接受或拒绝数据包。
3-20. 这些行定义了BPF过滤器的具体指令。每个指令由四个字节组成,分别代表操作码、源寄存器、目标寄存器和立即数。这里是一个简化的指令解释:
{ 0x28, 0, 0, 0x0000000c },
:LOAD_ABSOLUTE,将偏移量0xc处的4个字节加载到寄存器A。{ 0x15, 0, 8, 0x00000800 },
:JEQ(等于),如果寄存器A的值等于0x0800(IPv4头部),则跳转到偏移量8处的指令。{ 0x30, 0, 0, 0x00000017 },
:LOAD_ABSOLUTE,将偏移量0x17处的4个字节加载到寄存器A。{ 0x15, 0, 6, 0x00000011 },
:JEQ(等于),如果寄存器A的值等于0x0011(UDP协议),则跳转到偏移量6处的指令。{ 0x28, 0, 0, 0x00000014 },
:LOAD_ABSOLUTE,将偏移量0x14处的4个字节加载到寄存器A。{ 0x45, 4, 0, 0x00001fff },
:JGT(大于),如果寄存器A的值大于0x1fff(UDP源端口在高位字节),则跳转到偏移量0处的指令。{ 0xb1, 0, 0, 0x0000000e },
:ADD,将寄存器A的值加上0xe。{ 0x48, 0, 0, 0x0000000e },
:LD_W,从内存中加载4个字节到寄存器A,地址为寄存器A的值加0xe。{ 0x15, 0, 1, 0x00000035 },
:JEQ(等于),如果寄存器A的值等于0x0035(源端口为53),则跳转到偏移量1处的指令。{ 0x6, 0, 0, 0x00040000 },
:RET,返回并允许该数据包通过过滤器。{ 0x6, 0, 0, 0x00000000 },
:RET,返回并阻止该数据包通过过滤器。
struct sock_fprog bpf = { ... };
:定义一个sock_fprog
结构体变量bpf
,用于存储BPF过滤器的信息,包括指令集长度和指令集本身。
sock_fprog
是 Linux 内核中另一个与 Berkeley Packet Filter (BPF) 相关的结构。它用于封装一个 BPF 程序,并在设置 socket 层的数据包过滤时使用。
sock_fprog
结构通常包含以下字段:struct sock_fprog {unsigned short len; // BPF 程序的指令数struct sock_filter *filter; // 指向 BPF 程序指令数组的指针 };
以下是对每个字段的简要解释:
len
: 此字段表示 BPF 程序中的指令数量。它告诉内核需要执行多少条 BPF 指令来过滤数据包。
filter
: 这是一个指向sock_filter
结构数组的指针。这个数组包含了实际的 BPF 程序指令,这些指令将被用来检查和过滤网络数据包。当需要在套接字级别应用数据包过滤时,可以创建一个
sock_fprog
结构实例,并通过系统调用(如setsockopt()
)将其传递给内核。内核然后会使用这个 BPF 程序来决定是否应该允许数据包通过或者丢弃。这种机制常用于实现轻量级的、在内核中运行的网络数据包过滤,例如在防火墙、网络监控工具或者性能分析工具中。
-
bpf.len = (unsigned short) sizeof(code) / sizeof(code[0]),
:设置bpf
结构体中的len
字段,表示BPF过滤器的指令数量。 -
bpf.filter = code,
:设置bpf
结构体中的filter
字段,指向BPF过滤器的指令集。 -
int rv = setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));
:调用setsockopt
系统调用,将BPF过滤器附加到给定的套接字上。如果成功,返回值rv
应大于等于0。
25-27. if (rv < 0) { ... }
:检查setsockopt
的返回值,如果小于0(表示失败),则输出错误信息并退出程序。
-
fprintf(stderr, "Error: cannot attach BPF filter\n");
:向标准错误输出流输出错误信息。 -
exit(1);
:终止程序执行,并返回1作为退出状态码。 -
}
:结束custom_bpf
函数的定义。
print_date
这段代码定义了一个名为print_date
的静态函数,其功能是在每次日期发生变化时打印当前的日期和时间。
以下是对每一行代码的详细解释:
-
static void print_date(void) {
:定义一个静态函数print_date
,该函数没有参数和返回值。 -
static int day = -1;
:定义一个静态整型变量day
,并初始化为-1。这个变量用于存储前一天的年天数(一年中的第几天),初始值为-1表示这是程序运行以来的第一次调用。 -
time_t now = time(NULL);
:调用time
函数获取当前的时间(以秒为单位),并将结果存储在time_t
类型的变量now
中。 -
struct tm *t = localtime(&now);
:调用localtime
函数将now
转换为本地时间,并将结果存储在一个指向struct tm
的指针t
中。struct tm
包含了日期和时间的各种信息,如年、月、日、小时、分钟、秒等。 -
if (day != t->tm_yday) { ... }
:检查当前的年天数(t->tm_yday
)是否与前一天的年天数(day
)不同。如果不同,则执行花括号内的代码。 -
printf("DNS trace for %s", ctime(&now));
:调用printf
函数打印一条消息,内容为"DNS trace for"后面跟着由ctime
函数生成的日期和时间字符串。ctime
函数将time_t
类型的变量转换为人类可读的日期和时间字符串。 -
day = t->tm_yday;
:将当前的年天数(t->tm_yday
)赋值给day
变量,以便下次调用函数时进行比较。 -
}
:结束if
语句的花括号。 -
fflush(0);
:调用fflush
函数刷新标准输出流(文件描述符为0,通常对应于屏幕)。这确保任何缓冲的数据都被立即写入到输出设备。 -
}
:结束print_date
函数的定义。
总的来说,这个函数的作用是在每次日期发生变化时输出一个新的日期和时间行,用于标记DNS跟踪的日志记录。通过使用静态变量day
来保存前一天的年天数,可以避免在同一天内多次打印日期。
run_trace
这段代码定义了一个名为run_trace
的静态函数,其功能是捕获所有以太网数据包,并使用自定义的BPF过滤器筛选出源端口为53的UDP数据包。然后对这些数据包进行解析,提取DNS查询信息并打印。
以下是对每一行代码的详细解释:
-
static void run_trace(void) {
:定义一个静态函数run_trace
,该函数没有参数和返回值。 -
// grab all Ethernet packets and use a custom BPF filter to get only UDP from source port 53
:注释说明了函数的主要功能。 -
int s = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
:创建一个原始套接字,用于接收所有类型的以太网数据包。PF_PACKET
表示协议家族为packet,SOCK_RAW
表示原始套接字类型,htons(ETH_P_ALL)
指定要接收所有以太网协议类型的数据包。 -
if (s < 0) errExit("socket");
:检查套接字是否成功创建,如果小于0则调用errExit
函数输出错误信息并退出程序。 -
custom_bpf(s);
:调用custom_bpf
函数为套接字s
设置自定义的BPF过滤器,用于筛选源端口为53的UDP数据包。
6-7. struct timeval tv;
和 tv.tv_sec = 10; tv.tv_usec = 0;
:定义一个timeval
结构体变量tv
,并设置其tv_sec
字段为10秒(等待时间),tv_usec
字段为0微秒。
-
unsigned char buf[MAX_BUF_SIZE];
:定义一个无符号字符数组buf
,用于存储接收到的数据包。 -
while (1) { ... }
:无限循环,持续接收和处理数据包。
10-12. fd_set rfds; FD_ZERO(&rfds); FD_SET(s, &rfds);
:定义一个文件描述符集合rfds
,清零并添加套接字s
到集合中。
int rv = select(s + 1, &rfds, NULL, NULL, &tv);
:调用select
函数等待套接字s
变为可读状态,或者等待超时(由tv
指定)。返回值rv
表示就绪的文件描述符数量。
14-15. if (rv < 0) errExit("select"); else if (rv == 0) { ... }
:检查select
的返回值,如果小于0则调用errExit
函数输出错误信息并退出程序;如果等于0,则表示超时,执行花括号内的代码。
-
print_date();
:调用print_date
函数打印当前日期和时间。 -
tv.tv_sec = 10; tv.tv_usec = 0;
:重置tv
的时间值为10秒。 -
continue;
:结束本次循环,进入下一轮循环。 -
unsigned bytes = recvfrom(s, buf, MAX_BUF_SIZE, 0, NULL, NULL);
:从套接字s
接收数据包,并将数据存入buf
中。返回值bytes
表示接收到的数据长度。
20-21. if (bytes >= (14 + 20 + 8)) { ... }
:检查接收到的数据长度是否大于等于MAC头部(14字节)、IP头部(20字节)和UDP头部(8字节)的总长度,如果是则继续执行。
-
uint8_t ip_hlen = (buf[14] & 0x0f) * 4;
:根据IP头部中的第一个字节计算IP头部的实际长度,并将结果存储在ip_hlen
变量中。 -
uint16_t port_src; memcpy(&port_src, buf + 14 + ip_hlen, 2); port_src = ntohs(port_src);
:从数据包中提取源端口号,并将其转换为主机字节序。 -
uint8_t protocol = buf[14 + 9];
:从数据包中提取上层协议类型(如TCP、UDP等)。 -
uint32_t ip_src; memcpy(&ip_src, buf + 14 + 12, 4); ip_src = ntohl(ip_src);
:从数据包中提取源IP地址,并将其转换为主机字节序。
26-32. if (arg_nolocal) { ... }
:如果arg_nolocal
标志为真,则检查源IP地址是否属于本地网络范围(如127.0.0.0/8、10.0.0.0/8、192.168.0.0/16或172.16.0.0/12),如果是则跳过本次循环。
33-35. if (port_src == 53 && protocol == 0x11) // UDP protocol print_dns(ip_src, buf + 14 + ip_hlen + 8); // IP and UDP header len
:如果源端口号为53且上层协议为UDP,则调用print_dns
函数解析DNS查询信息并打印。
-
}
:结束while
循环。 -
close(s);
:关闭套接字s
。 -
}
:结束run_trace
函数的定义。
main
这段代码是程序的主函数,其功能是处理命令行参数、检查运行权限并启动数据包捕获和解析。
以下是对每一行代码的详细解释:
-
int main(int argc, char **argv) {
:定义主函数main
,接受两个参数:命令行参数的数量(argc
)和指向参数字符串数组的指针(argv
)。 -
int i;
:定义一个整型变量i
,用于遍历命令行参数。
3-5. for (i = 1; i < argc; i++) { ... }
:使用for
循环遍历命令行参数,从索引为1的参数开始(索引0通常为程序名)。
6-8. if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-?") == 0) { ... }
:检查当前参数是否为"–help"或"-?",如果是则调用usage
函数打印帮助信息,并返回0(表示程序正常结束)。
9-10. else if (strcmp(argv[i], "--nolocal") == 0) arg_nolocal = 1;
:检查当前参数是否为"–nolocal",如果是则将全局变量arg_nolocal
设置为1,表示不显示本地网络范围内的DNS查询。
11-13. else { ... }
:如果当前参数既不是帮助选项也不是--nolocal
,则输出错误信息并返回1(表示程序异常结束)。
}
:结束else
语句。
15-19. if (getuid() != 0) { ... }
:检查当前用户的用户ID是否为0(即root用户)。如果不是,则输出错误信息并返回1。
-
prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0);
:调用prctl
系统调用,设置进程的父死亡信号为SIGKILL
。这意味着当父进程终止时,该进程也会被立即杀死。 -
print_date();
:调用print_date
函数打印当前日期和时间。 -
run_trace();
:调用run_trace
函数开始捕获和解析数据包。 -
return 0;
:返回0,表示程序正常结束。 -
}
:结束main
函数的定义。
补充DNS
DNS协议报文格式
2.1 头部
-
**会话标识(2字节):**是DNS报文的ID标识,对于请求报文和其对应的应答报文,这个字段是相同的,通过它可以区分DNS应答报文是哪个请求的响应
-
标志(2字节):
[QR(1bit) 查询/响应标志,0为查询,1为响应 opcode(4bit) 0表示标准查询,1表示反向查询,2表示服务器状态请求 AA(1bit) 表示授权回答 TC(1bit) 表示可截断的 RD(1bit) 表示期望递归 RA(1bit) 表示可用递归 rcode(4bit) 表示返回码,0表示没有差错,3表示名字差错,2表示服务器错误(Server Failure) -
**数量字段(总共8字节):**Questions、Answer RRs、Authority RRs、Additional RRs 各自表示后面的四个区域的数目。Questions表示查询问题区域节的数量,Answers表示回答区域的数量,Authoritative namesversers表示授权区域的数量,Additional recoreds表示附加区域的数量
2.2 正文
-
Queries区域
[
1.1 查询名:长度不固定,且不使用填充字节,一般该字段表示的就是需要查询的域名(如果是反向查询,则为IP,反向查询即由IP地址反查域名),一般的格式如下图所示。
1.2 查询类型:
类型 助记符 说明 1 A 由域名获得IPv4地址 2 NS 查询域名服务器 5 CNAME 查询规范名称 6 SOA 开始授权 11 WKS 熟知服务 12 PTR 把IP地址转换成域名 13 HINFO 主机信息 15 MX 邮件交换 28 AAAA 由域名获得IPv6地址 252 AXFR 传送整个区的请求 255 ANY 对所有记录的请求 这里给一个域名,可用来模拟DNS的查询类型,可以选择不同的类型,比如A,PTR等玩一下, https://www.nslookuptool.com/chs/
*1.3 查询类:通常为1,表明是Internet数据* -
资源记录(RR)区域(包括回答区域,授权区域和附加区域)
[
该区域有三个,但格式都是一样的。这三个区域分别是:回答区域,授权区域和附加区域
2.1. 域名(2字节或不定长):它的格式和Queries区域的查询名字字段是一样的。有一点不同就是,当报文中域名重复出现的时候,该字段使用2个字节的偏移指针来表示。比如,在资源记录中,域名通常是查询问题部分的域名的重复,因此用2字节的指针来表示,具体格式是最前面的两个高位是 11,用于识别指针。其余的14位从DNS报文的开始处计数(从0开始),指出该报文中的相应字节数。一个典型的例子,C00C
(1100000000001100**,**12正好是头部的长度,其正好指向Queries区域的查询名字字段)。
**2.2 查询类型:**表明资源纪录的类型,见1.2节的查询类型表格所示
**2.3 查询类:**对于Internet信息,总是IN
**2.4 生存时间(TTL):**以秒为单位,表示的是资源记录的生命周期,一般用于当地址解析程序取出资源记录后决定保存及使用缓存数据的时间,它同时也可以表明该资源记录的稳定程度,极为稳定的信息会被分配一个很大的值(比如86400,这是一天的秒数)。
2.5. 资源数据:该字段是一个可变长字段,表示按照查询段的要求返回的相关资源记录的数据。可以是Address(表明查询报文想要的回应是一个IP地址)或者CNAME(表明查询报文想要的回应是一个规范主机名)等。
报文格式
DNS 一般采用 UDP 作为传输层协议( TCP 亦可),端口号 是 53 。请求报文和应答报文均作为数据,搭载在 UDP 数据报中进行传输。
DNS 报文分为 请求 和 应答 两种,结构是类似的,大致分为五部分:
- 头部( header ),描述报文类型;
- 问题节( question ),保存查询问题;
- 答案节( answer ),保存问题答案,也就是查询结果;
- 授权信息节( authority ),保存授权信息;
- 附加信息节( additional ),保存附加信息;
其中,头部是固定的,共 12 字节;其他节不固定,记录数可多可少,数目保存在头部中。头部分为 6 个字段:
- 标识( identifier ),一个 16 位的 ID ,在应答中原样返回,以此匹配请求和应答;
- 标志( flags ),一些标志位,共 16 位;
- 问题记录数( question count ),一个 16 位整数,表示问题节中的记录个数;
- 答案记录数( answer count ),一个 16 位整数,表示答案节中的记录个数;
- 授权信息记录数( authority record count ),一个 16 位整数,表示授权信息节中的记录个数;
- 附加信息记录数( additional record count ),一个 16 位整数,表示附加信息节中的记录个数;
最后,我们来解释一下标志字段中的各个标志位:
-
QR 位标记报文是一个查询请求,还是查询应答;
- 0 表示查询请求;
- 1 表示查询应答;
-
操作码( opcode )占 4 位,表示操作类型:
- 0 代表标准查询;
- 1 代表反向查询;
- 2 代表服务器状态请求;
-
AA 位表示 权威回答( authoritative answer ),意味着当前查询结果是由域名的权威服务器给出的;
-
TC 位表示 截断( truncated ),使用 UDP 时,如果应答超过 512 字节,只返回前 512 个字节;此时将自动切换使用 TCP 进行查询。
-
RD 位表示 期望递归 ( recursion desired ),在请求中设置,并在应答中返回;
- 该位为 1 时,服务器必须处理这个请求:如果服务器没有授权回答,它必须替客户端请求其他 DNS 服务器,这也是所谓的 递归查询 ;
- 该位为 0 时,如果服务器没有授权回答,它就返回一个能够处理该查询的服务器列表给客户端,由客户端自己进行 迭代查询 ;
-
RA 位表示可递归( recursion available ),如果服务器支持递归查询,就会在应答中设置该位,以告知客户端;
-
保留位,这 3 位目前未用,留作未来扩展;
-
响应码( response code )占 4 位,表示请求结果,常见的值包括:
- 0 表示没有差错;
- 3 表示名字差错,该差错由权威服务器返回,表示待查询的域名不存在;
问题记录
客户端查询域名时,需要向服务端发送请求报文;待查询域名作为问题记录,保存在问题节中。
问题节支持保存多条问题记录,记录条数则保存在 DNS 头部中的问题记录数字段。这意味着,DNS 协议单个请求能够同时查询多个域名,虽然通常只查询一个。
一个问题记录由 3 个字段组成:
-
待查询域名( Name ),这个字段长度不固定,由具体域名决定;
-
查询类型( Type ),域名除了关联 IP 地址,还可以关联其他信息,常见类型包括:
- 1 表示 A 记录,即 IP 地址;
- 28 表示 AAAA 记录,即 IPv 6 地址;
- Etc…
-
类 ( Class )通常为 1 ,表示 TCP/IP 互联网地址;
最后,我们回过头来考察域名字段,它的长度是不固定的。域名按 .
切分成若干部分,再依次保存。每个部分由一个前导计数字节开头,记录当前部分的字符数。
以域名 fasionchan.com.
为例,以 .
切分成 3 个部分,fasionchan
、com
以及空字符串。请注意,空字符串代表根域。因此,待查询域名字段依次为:
- 一个前导字节保存整数 10 ,然后 10 个字节保存
fasionchan
部分(二级域); - 一个前导字节保存整数 3 ,然后 3 个字节保存
com
部分(一级域); - 一个前导字节保存整数 0 ,然后 0 个字节保存部分(根域);
由此可见,每一级域名的长度理论上可以支持多达 255 个字符。
资源记录
服务端处理查询请求后,需要向客户端发送应答报文;域名查询结果作为资源记录,保存在答案以及其后两节中。
答案节、授权信息节和附加信息节均由一条或多条资源记录组成,记录数目保存在头部中的对应字段,不再赘述。
资源记录结构和问题记录非常相似,它总共有 6 个字段,前 3 个和问题记录完全一样:
- 被查询域名( Name ),与问题记录相同;
- 查询类型( Type ),与问题记录相同;
- 类 ( Class ),与问题记录相同;
- 有效期( TTL ),域名记录一般不会频繁改动,所以在有效期内可以将结果缓存起来,降低请求频率;
- 数据长度( Resource Data Length ),即查询结果的长度;
- 数据( Resource Data ),即查询结果;
如果查询类型是 A 记录,那查询结果就是一个 IP 地址,保存于资源记录中的数据字段;而数据长度字段值为 4 ,因为 IP 地址的长度为 32 位,折合 4 字节。
报文实例
以 test.fasionchan.com
这个域名为例,来讲解 DNS 查询请求报文和应答报文。
执行 dig 命令即可查询该域名:
dig test.fasionchan.com
对查询 test.fasionchan.com
的一次通信过程进行抓包,结果保存在 Github 上,以供参考。童鞋们可以将抓包结果下载到本地,并用 WireShark 打开,并结合讲解进行分析。
请求报文
抓包结果请求报文只有头部、问题节和附加节,图解假设没有附加节。
先看头部,问题记录数为 1 ,其他记录数都是 0 。这意味着,请求报文只有问题节,而且问题节中只有一条问题记录,只查询一个域名。头部中的标志位分别如下:
- QR=0 ,表示该报文是一个请求报文;
- 操作码为 0 ,表示这个 DNS 请求是一个标准请求;
- TC=0 ,表示请求报文没有被截断;
- RD=1 ,表示客户端希望服务器可以执行递归查询;
问题记录我们已经很熟悉了,不再赘述:
- Type=1 ,表示客户端希望查询 A 记录,即与域名关联的 IP 地址;
- Class=1 ,代表 TCP/IP 互联网;
响应报文
抓包结果应答报文只有头部、问题节和答案节。其中,问题节中的问题记录与请求报文一样,图中就不展开了。
先看头部,问题记录数为 1 ,答案记录数也是 1 ,其他记录数都是 0 。这意味着,应答报文只有问题节和答案节,而且它们各自只有一条记录。头部中的标志位分别如下:
- QR=1 ,表示该报文是一个应答报文;
- 操作码为 0 ,表示这个 DNS 请求是一个标准请求;
- AA=0 ,表示结果不是由域名的权威服务器返回的,因为查询对象是本地的 DNS 缓存服务器(如果是向权威服务器发起查询,它返回的应答报文 AA=1 );
- TC=0 ,表示应答报文没有被截断;
- RD=1 ,与请求报文保持一致,略;
- RA=1 ,表示服务端支持递归查询;
- 响应码为 0 ,表示查询成功,没有出错;
答案节中的资源记录就是查询结果,前 3 个字段与问题记录一样,不再赘述。
TTL 字段是一个整数,表示有效期,单位是秒。例子中的查询结果,有效期是 752 秒,即 12 分 32 秒。也就是说,查询结果从现在开始算,12 分 32 秒内均有效,无须重新请求。
查询结果是一个 IP 地址,长度为 4 个字节,保存在资源数据字段中。
DNS解析过程
我们平时在访问网站时,不使用 IP 地址,而是网站域名。但是抓包发现:交互报文是以 IP 地址进行的。那么 IP 地址是从哪来的呢?这是因为 DNS 把网站域名自动转换为 IP 地址。
DNS 出现
TCP/IP 是基于 IP 地址进行通信的,但是 IP 地址不太好记。于是出现了另一种方便记忆的标识符,那就是主机名。为计算机配置主机名,在进行网络通信时,直接使用主机名,而不用输入一大串的 IP 地址。同时,系统通过一个叫 hosts 的文件,实现主机名转换 IP 地址的功能。***hosts 文件包括主机名和 IP 地址的对应关系。***当需要通过主机名访问主机时,它就会查看本地的 hosts 文件,从文件中找到相对应的 IP 地址,然后进行报文发送。如果在 hosts 文件中没找到相关信息,则主机访问失败。
hosts 文件是主机的本地文件,优点是查找响应速度快。它主要用来存储一些本地网络的主机名和 IP 地址的对应信息。这样,主机在以主机名访问本地网络主机时,通过 hosts 文件可以迅速获得相应的 IP 地址。
每台主机的 hosts 文件都需要单独手工更新。随着网络规模的不断扩大、接入计算机的数量不断增加,维护难度越来越大,每台主机同步更新,几乎是一件不可能完成的任务。
为了解决 hosts 文件维护困难的问题,出现了 DNS 域名系统,一个可以解决主机名和 IP 地址互相转换的系统。*无论网络规模变得多么庞大,都能在一个小范围内通过 DNS 进行管理。*
DNS 介绍
DNS ,全称 Domain Name System
。采用 client/server
模式,DNS client
发出查询请求,DNS server
响应请求。DNS client
通过查询 DNS server
获得主机的 IP 地址,进而完成后续的 TCP/IP 通信过程。
当 Windows 系统用户使用 nslookup hostname/domainname
命令时,DNS 会自动查找注册了主机名和 IP 地址的数据库,并返回对应的 IP 地址。
DNS 域名
先了解什么是域名,才能理解 DNS 。*域名是为了识别主机名或机构的一种分层的名称。*因为单独的一台域名服务器是不可能知道所有域名信息,所以域名系统是一个分布式数据库系统,域名(主机名)到 IP 地址的解析可以由若干个域名服务器共同完成。每一个站点维护自己的信息数据库,并运行一个服务器程序供互联网上的客户端查询。DNS 提供了客户端与服务器的通信协议,也提供了服务器之间交换信息的协议。由于是分布式系统,即使单个服务器出现故障,也不会导致整个系统失效,消除了单点故障。
DNS 域名组成
DNS 域的本质是一种管理范围的划分,最大的域是根域,向下可以划分为顶级域、二级域、三级域、四级域等。相对应的域名是根域名、顶级域名、二级域名、三级域名等。不同等级的域名使用点号分隔,级别最低的域名写在最左边,而级别最高的域名写在最右边。
举个栗子:网站域名 http://www.tsinghua.edu.cn 中,从右到左开始,cn
是顶级域名,代表中国,edu
是二级域名,代表教育机构,tsinghua
是三级域名,表示清华大学,www
则表示三级域名中的主机,并提供了 web 服务。
除了 www
主机外,常见的主机还有 arch
、sem
、mail
,域名如下:
每一级的域名都由英文字母和数字组成,域名不区分大小写,长度不能超过 63 字节,一个完整的域名不能超过 255 个字节。根域名用 “ . ”(点)表示。目前我们看到的域名例子都是完全合格域名( FQDN ),FQDN 的完整格式是以点结尾的域名。接入互联网的主机、服务器或其它网络设备都可以拥有一个唯一的 FQDN 。与 FQDN 对应的,系统中的默认域名是非合格域名,会把当前的区域域名添加到尾部。例如,tsinghua 域内的主机上查找 mail
,本地解析器就会将这个名称转换为 FQDN ,即 http://mail.tsinghua.edu.cn ,然后解析出 IP 地址。
DNS 域名空间
域名空间结构像是一棵倒过来的树,也叫做树形结构。根域名就是树根( root ),用点号表示,往下是这棵树的各层枝叶。根域名的下一层叫**顶级域名**,顶级域名包括三大类:
- 国家顶级域名
国家顶级域名采用 ISO3166 的规定。比如:.cn
表示中国,.us
表示美国,.uk
表示英国等。现在使用的国家顶级域名大约在 200个左右。
- 国际顶级域名
国际顶级域名采用 .int
。国际性的组织可以在 .int
下注册。
- 通用顶级域名
最早的顶级域名共有 *6* 个。分别为:
.com
表示公司企业,
.net
表示网络服务机构,
.org
表示非盈利组织,
.edu
表示教育机构(仅限美国),
.gov
表示政府部门(仅限美国),
.mil
表示军事部门(仅限美国)。
随着互联网用户不断增加,又增加了 *7* 个通用顶级域名。分别为:
.aero
用于航空运输业,
.biz
用于公司和企业,
.coop
用于合作团体,
.info
用于各种情况,
.museum
用于博物馆,
.name
用于个人,
.pro
用于自由职业者。
顶级域名下面是二级域名。国家顶级域名下注册的二级域名均由国家自行确定。我国二级域名分为类别域名和行政域名两大类,类别域名如:.com
,.edu
,.gov
等分别代表不同的机构;行政域名如:.bj
表示北京,.sh
表示上海,代表我国各省、自治区及直辖市等。
二级域名下面是三级域名、四级域名等。*命名树上任何一个节点的域名就是从这个节点到最高层的域名串起来,中间以 “ . ” 分隔。*
在域名结构中,节点在所属域中的主机名标识可以相同,但是*域名必须不同*。比如:清华大学和新浪公司下都有一台主机的标识是 mail
,但是两者的域名却是不同的,前者为 http://mail.tsinghua.edu.cn ,而后者为 http://mail.sina.com.cn 。
DNS 域名解析
将域名转换为对应的 IP 地址的过程叫做域名解析。在域名解析过程中,DNS client
的主机调用解析器( Resolver ),向 DNS server
发出请求,DNS server
完成域名解析。
域名解析是按照 DNS 分层结构的特点,自顶向下进行的。但是如果每一个域名解析都从根域名服务器开始,那么根域名服务器有可能无法承载海量的流量。在实际应用中,大多数域名解析都是在本地域名服务器完成。通过合理设置本地域名服务器,由本地域名服务器负责大部分的域名解析请求,提高域名解析效率。
DNS 解析器
从应用程序的角度看,访问 DNS 是通过一个叫**解析器**( Resolver )的应用程序来完成的。发送一个 TCP 或 UDP 数据包之前,解析器必须将域名(主机名)转换为 IP 地址。一个解析器至少要注册一个域名服务器的 IP 地址。通常,它至少包括本地域名服务器的 IP 地址。
DNS 域名服务器
DNS 域名空间的层次结构,允许不同的域名服务器管理域名空间的不同部分。域名服务器是指管理域名的主机及软件,它可以管理所在分层的域。其所管理的分层叫做区域( zone )。一个 zone 是 DNS 域名空间的一棵子树,它可以单独管理而不受其它 zone 影响。*每层都设有一个域名服务器。*
根所设置的 DNS 叫做根域名服务器,它对 DNS 的检索数据功能起着至关重要的作用。根域名服务器中注册了顶级域名服务器的 IP 地址。如果想要新增一个一级域名,或者修改已有的顶级域名,就要在根域名服务器中进行新增或变更。
类似的,顶级域名服务器中注册了二级域名服务器的 IP 地址。如果域名服务器下面没有其它分层,就可以自由地指定主机名称。如果想重新设置域名服务器的 IP 地址或修改域名,必须在上一层的域名服务器中进行修改。
域名和域名服务器都需要按照分层进行设置。如果域名服务器出现故障,那么针对这个域的 DNS 查询就无法正常工作。因此,为了提高可用性,至少设置两台域名服务器。一旦第一台域名服务器无法提供查询时,就会自动转到第二个甚至第三个域名服务器上进行。
DNS 域名服务器类型
- 本地域名服务器
互联网接入服务运营商或者一个大的网络机构,像公司、大学等都有一台或多台可以自行管理的域名服务器,这类域名服务器称为**本地域名服务器**,也称为默认域名服务器。本地域名服务器离客户端较近。当一个 DNS客户端发出 DNS 查询时,首先送到本地域名服务器。如果本地域名服务器数据库中有对应的域名信息,会将查询的域名转换为 IP 地址返回客户端。如果没有,它会以 DNS 客户端的身份向根域名服务器进行查询。根域名服务器收到本地域名服务器的查询后,会返回相关域名服务器的 IP 地址,本地域名服务器再向相关域名服务器发送查询请求。
- 根域名服务器
通常根域名服务器用来管理顶级域,本身并不对域名进行解析,但它知道相关域名服务器的 IP 地址。IPv4 根域名服务器全球有 13 台,主机名分别为 A ~ M 。1 台为主根服务器在美国,其余 12 个均为辅根服务器,其中 9 个在美国,2 个在欧洲,位于英国和瑞典,1 台在亚洲,位于日本。*所有的域名服务器都必须注册根域名服务器的 IP 地址*,因为 DNS 根据 IP 地址进行检索时,需要按顺序从根域名服务器开始。
- 授权域名服务器
互联网上的主机在域名服务器上进行注册,这个域名服务器就是主机的授权域名服务器。通常,主机的授权域名服务器就是本地域名服务器。实际上,主机会有两个授权域名服务器,防止单点故障。授权域名服务器上有注册主机域名与 IP 地址的映射信息,当查询注册主机域名时,它会返回相应主机的 IP 地址。如果主机域名和 IP 地址需要进行变更,只需要在授权域名服务器处理即可,不用再向其它域名服务器进行申请或报告。
- 主域名服务器
主域名服务器是完成一个或多个区域域名解析工作的主要域名服务器,通常也是一个或多个区域的授权域名服务器。主域名服务器有区域内主机地址信息的源数据文件,并且是区域传送中区域数据的唯一来源。
- 辅助域名服务器
辅助域名服务器可以协助主域名服务器提供域名查询服务,在主机很多的情况下,可以有效分担主域名服务器的压力。辅助域名服务器也有冗余功能,当主域名服务器故障时,辅助域名服务器能够在数据有效期内继续为主机提供域名解析服务。
一台主域名服务器可以有多台辅助域名服务器,一台辅助域名服务器也可以是其它区域的主域名服务器。辅助域名服务器中包含区域内主机地址数据的授权信息,通过区域配置文件副本的方式存储。辅助域名服务器也是区域的授权域名服务器,可以完成本区域内域名查询的授权回答。
辅助域名服务器并不建立数据文件,它获得区域数据的唯一途径就是通过区域传送的方式,从主域名服务器上获得区域数据的最新副本。获得数据副本的方式有两种,第一种是辅助域名服务器定期主动获得主域名服务器的副本,或更新副本中的数据。第二种是主域名服务器在区域数据发生变化时,及时通知辅助域名服务器更新副本中的数据。
DNS 域名完整解析过程
DNS 客户端进行域名 http://www.tsinghua.edu.cn 的解析过程如下:
- DNS 客户端向本地域名服务器发送请求,查询 http://www.tsinghua.edu.cn 主机的 IP 地址;
- 本地域名服务器查询数据库,发现没有域名为 http://www.tsinghua.edu.cn 的主机,于是将请求发送给根域名服务器;
- 根域名服务器查询数据库,发现没有这个主机域名记录,但是根域名服务器知道 cn 域名服务器可以解析这个域名,于是将 cn 域名服务器的 IP 地址返回给本地域名服务器;
- 本地域名服务器向 cn 域名服务器查询 http://www.tsinghua.edu.cn 主机的 IP 地址;
- cn 域名服务器查询数据库,也没有相关记录,但是知道 http://edu.cn 域名服务器可以解析这个域名,于是将 http://edu.cn 域名服务器的 IP 地址返回给本地域名服务器;
- 本地域名服务器再向 http://edu.cn 域名服务器查询 http://www.tsinghua.edu.cn 主机 IP 地址;
- http://edu.cn****域名服务器查询数据库,也没有相关记录,但是知道 http://tsinghua.edu.cn 域名服务器可以解析这个域名,于是将 http://tsinghua.edu.cn 的域名服务器 IP 地址返回给本地域名服务器;
- 本地域名服务器向 http://tsinghua.edu.cn 域名服务器查询 http://www.tsinghua.edu.cn 主机的 IP 地址;
- http://tsinghua.edu.cn****域名服务器查询数据库,发现有主机域名记录,于是给本地域名服务器返回 http://www.tsinghua.edu.cn 对应的 IP 地址;
- 最后本地域名服务器将 http://www.tsinghua.edu.cn 的 IP 地址返回给客户端,整个解析过程完成。
DNS 传输层协议
DNS 域名服务器使用的端口号是 53 ,并且同时支持 UDP 和 TCP 协议。为什么同时使用两种协议呢?
因为 DNS 响应报文中有一个删减标志位,用 TC 表示。当响应报文使用 UDP 封装,且报文长度大于 512 字节时,那么服务器只返回前 512 字节,同时 TC 标志位置位,表示报文进行了删减。当客户端收到 TC 置位的响应报文后,将采用 TCP 封装查询请求。DNS 服务器返回的响应报文长度大于 512 字节。
UDP 报文的最大长度是 512 字节,最多可以包含 13 台根域名服务器数据,因此 ipv4 根域名服务器只能限制在 13 个,且每个服务器使用单个字母命名,也是 IPv4 根服务器是从 A ~ M 命名的原因。
当辅助域名服务器启动时,将从主域名服务器执行区域传送。正常运行过程中,辅助域名服务器也会定时向主域名服务器进行查询,以便了解主域名服务器数据是否发送变化。如果有变化,将执行一次区域传送。因为区域传送的数据多,所以传送的数据采用 TCP 封装。
因此,*UDP 用于 client 和 server 的查询和响应*,*TCP 用于主从 server 之间的传送*。
DNS 查询方式
DNS 域名解析包括两种查询( query )方式,一种是递归查询,另一种是*迭代查询*。
递归查询
DNS 服务器如果不能直接响应解析请求,它将继续请求其它的 DNS 服务器,直到查询域名解析的结果。查询的结果可以是域名主机的 IP 地址,或者是域名无法解析。*无论哪种结果,DNS 服务器都会将结果返回给客户端。*
举个栗子:当本地域名服务器接收了客户端的查询请求,本地域名服务器将代表客户端来找答案,而在本地域名服务器执行工作时,客户端只是等待,直到本地域名服务器将最终查询结果返回客户端。
迭代查询
如果 DNS 服务器查不到相应记录,会向客户端返回一个可能知道结果的域名服务器 IP 地址,由客户端继续向新的服务器发送查询请求。对域名服务器的迭代查询,只得到一个提示,则继续查询。
举个栗子:本地域名服务器发送请求到根域名服务器,根域名服务器并没有相应记录,它只是给本地域名服务器返回一个提示,引导本地域名服务器到另一台域名服务器进行查询。
客户端在查询 IP 地址时,向本地域名服务器进行递归查询。如果本地域名服务器的数据库有相应数据,则直接返回相应数据。如果没有,则本地域名服务器向根域名服务器进行迭代查询。从根开始对这棵树按照顺序进行遍历,直到找到指定的域名服务器,并由这个域名服务器返回相应的数据。*客户端和本地域名服务器会将收到的信息保存在缓存里*,这样可以减少每次查询时的性能消耗。
DNS 反向查询
在 DNS 查询中,客户端希望知道域名对应的 IP 地址,这种查询称为**正向查询。大部分的 DNS 查询都是正向查询。与正向查询对应的,是反向查询**。它允许 DNS 客户端通过 IP 地址查找对应的域名。
为实现反向查询,在 DNS 标准中定义了特色域 in-addr.arpa
域,并保留在域名空间中,以便执行反向查询。为创建反向域名空间,in-addr.arpa 域中的子域是按照 IP 地址相反的顺序构造的。
举个栗子:http://www.tsinghua.edu.cn 的 IP 地址是 166.111.4.100
,那么在 in-addr.arpa 域中对应的节点就是 100.4.111.166
。
DNS 应用场景
静态域名解析
域名解析分为动态域名解析和静态域名解析。在解析域名时,首先采用静态域名解析,如果静态解析不成功,再采用动态域名解析。
静态域名解析是通过静态域名解析表进行的,手动建立域名和 IP 地址之间的对应关系表,该表的作用类似于 Windows 操作系统下的 hosts 文件,可以将一些常用的域名放入表中。当 DNS client 需要域名所对应的 IP 地址时,即到静态域名解析表中去查找指定的域名,从而获得所对应的 IP 地址,提高域名解析的效率。
动态域名解析
**动态域名解析**需要专用的域名服务器( DNS server )运行域名解析服务器程序,提供从域名到 IP 地址的映射关系,负责接收客户端( DNS client)提出的域名解析请求。
DNS 代理
在使用了 DNS 代理( DNS proxy )功能的组网中,DNS client 将 DNS 请求报文直接发送给 DNS proxy 。DNS proxy 会先查找本地域名解析表,如果未查询到对应的解析表项,会将 DNS 请求报文转发给 DNS Server ,并在收到 DNS server 的应答报文后将其返回给 DNS client ,从而实现域名解析。
因此,当 DNS server 的地址发生变化时,只需改变 DNS proxy 上的配置,无需逐一改变局域网内每个 DNS client 的配置,从而简化了网络管理。
一些常用的域名放入表中。当 DNS client 需要域名所对应的 IP 地址时,即到静态域名解析表中去查找指定的域名,从而获得所对应的 IP 地址,提高域名解析的效率。
[外链图片转存中…(img-rh2a8Jjh-1702973915144)]
动态域名解析
**动态域名解析**需要专用的域名服务器( DNS server )运行域名解析服务器程序,提供从域名到 IP 地址的映射关系,负责接收客户端( DNS client)提出的域名解析请求。
[外链图片转存中…(img-idZCzhYN-1702973915144)]
DNS 代理
在使用了 DNS 代理( DNS proxy )功能的组网中,DNS client 将 DNS 请求报文直接发送给 DNS proxy 。DNS proxy 会先查找本地域名解析表,如果未查询到对应的解析表项,会将 DNS 请求报文转发给 DNS Server ,并在收到 DNS server 的应答报文后将其返回给 DNS client ,从而实现域名解析。
[外链图片转存中…(img-FHfGNhK8-1702973915145)]
因此,当 DNS server 的地址发生变化时,只需改变 DNS proxy 上的配置,无需逐一改变局域网内每个 DNS client 的配置,从而简化了网络管理。