首页>>前端>>Node->一文搞懂DNS协议是如何工作的!

一文搞懂DNS协议是如何工作的!

时间:2023-11-29 本站 点击:0

DNS是计算机网络中的一个应用层协议,它用于域名解析:将域名地址解析成对应的IP地址。 那么他是如何进行域名解析的呢?下面就来一起了解一下吧~

查阅本文可以学习到一下知识:

域名解析过程与方法

域名解析方法

DNS报文结构

实现一个简单的域名解析服务

域名组成

在开始之前,先介绍一下域名是怎么组成的

www.google.com为例,域名的读顺序是从右往左,右边com为顶级域名,googlewww则为标签(Label),com后的标签也称为二级域名

顶级域名 4  

顶级域名可以告诉用户域名所提供的服务类型。最通用的顶级域名(.com, .org, .net)不需要web服务器满足严格的标准,但一些顶级域名则执行更严格的政策。比如

地区的顶级域名,如.us,.fr,或.sh,可以要求必须提供给定语言的服务器或者托管在指定国家。这些TLD通常表明对应的网页服务从属于何种语言或哪个地区。

包含.gov的顶级域名只能被政府部门使用。

edu只能为教育或研究机构使用。

顶级域名既可以包含拉丁字母,也可以包含特殊字符。顶级域名最长可以达到63个字符,不过为了使用方便,大多数顶级域名都是两到三个字符。

标签(Label)4  

标签都是紧随着TLD的。标签由1到63个大小写不敏感的字符组成,这些字符包含字母A-z,数字0-9,甚至 “-” 这个符号(当然,“-” 不应该出现在标签开头或者标签的结尾)。

1.域名解析过程与方法

了解完域名的组成后,再来看DNS协议,DNS是一个基于UDP应用层协议。通过客户机-服务器的方式进行通信:也就是说,域名解析需要通过向服务器端发送请求,服务器端查询映射表或数据库后返回对应的IP地址给客户机(如图)。

DNS服务是按层次结构来检索域名的。

为什么需要使用层次结构呢?假设我们只有根域名服务器,那么全球所有的DNS解析都会涌入该服务器,这样做服务器的压力会非常大。

DNS层次结构如图所示

域名服务器分为4种:

根域名服务器:根域名服务器存储所有已经记录的域名,但是根域名服务器很少,只有几个。

顶级域名服务器:记录顶级域名下的所有域名,比如:com的顶级域名服务器会记录所有顶级域名为com的域名。

权威域名服务器: 负责一个区域的DNS解析的域名服务器。

本地域名服务器: 运行在本地的一个域名服务,用于向其他域名服务器请求解析的服务。

递归解析与迭代解析

域名解析流程:

在向DNS请求之前,本地域名服务器会先查询指定的域名是否有缓存,如果有且未过期,则立即返回。 如果没有,则向用户定义好的域名服务器查询。如下图

上面,我们也说到:DNS服务器是一个层级结构,多个域名服务器分布在不同的地方,但每次请求只能请求一个服务器。如果请求的域名服务器没有记录该域名对应的IP,那么他应该怎么向下一个域名服务器查询呢?

DNS查询提供了两种方法:迭代查询递归查询

递归查询

顾名思义,递归查询会在域名服务器查询记录的时候,继续向该域名服务器指定的下一级域名服务器查询,直到根域名服务器为止。

迭代查询

迭代查询在没有查询到对应的记录时,会将下一个可查询的域名服务器地址返回到本地域名服务器,由本地域名服务器重新发起请求查询,直到根域名服务器为止。

2.DNS报文结构

接下来查看一下DNS请求发送过来的报文数据是怎样的,首先我们创建一个UDP服务监听本地的53端口。然后在网络中设置DNS服务器指向本机的域名。

请求报文

首先分析一下DNS请求报文,为了方便查看,我们写一个DNS服务,去捕捉DNS请求

修改一下DNS解析的地址

建立一个UDP服务,监听53端口

const dgram = require('dgram');const server = dgram.createSocket('udp4');

server.on('error', (err) => { console.log('server error:', err.message); server.close(); });

server.on('message', (msg, rinfo) => { try { const dnsJson = JSON.parse(JSON.stringify(msg)); const dnsData = Array.from(dnsJson.data); console.log(dnsData) } catch {} });

server.on('listening', () => { const address = server.address(); console.log(server listening at ${address.port}); });

server.bind(53);

运行服务后在`命令行工具`中 ping一下`baidu.com`;![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b46230e2f7f14243bb7f5234e3c92b20~tplv-k3u1fbpfcp-watermark.image?)可以看到DNS解析的请求报文已经在node服务中打印出来了![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/21a1e7d470a24704a737adaa89dc0535~tplv-k3u1fbpfcp-watermark.image?)将他们转换为8位二进制数。![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/364d42af8a5645cb838d15d36a924d08~tplv-k3u1fbpfcp-watermark.image?)再来对照一下DNS报文结构。![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/85143cdf682744afbd296267ed6e6ade~tplv-k3u1fbpfcp-watermark.image?)* 事务ID:占两字节,对应16个二进制位,作用是作为`请求的标识`,即在数组中前两个数       `10001010 01110110`* 标志位:占两字节,16个二进制位,每一个二进制位都有其作用,每个标志位按顺序说明如下       `00000001 00000000`       | 标志 | 作用 |    | --- | --- |    | QR(1bit) | 查询(Query)/响应(Response)标志,0为查询,1为响应 |    | opcode(4bit)| 表示操作码,0 表示标准查询;1 表示反向查询;2 表示服务器状态请求 |    | AA(Authoritative)(1bit)| 授权应答,该字段在响应报文中有效。值为 1 时,表示名称服务器是权威服务器;值为 0 时,表示不是权威服务器。 |    | TC(Truncated)(1bit) | 表示是否被截断。值为 1 时,表示响应已超过 512 字节并已被截断,只返回前 512 个字节 |    | RD(Recursion Desired)(1bit)| 是否期望递归。0表示迭代查询,1表示递归查询 |    | RA(Recursion Available)(1bit) | 是否支持递归。该字段只出现在响应报文中。当值为 1 时,表示服务器支持递归查询 |    | ZERO(3bit) | 表示保留字段 |    | rcode(Reply code)(4bit) | 表示返回码,0表示没有差错,3表示名字差错,2表示服务器错误(Server Failure) |* 问题计数,对应数组中第 5 - 12 个数    | 字段                           | 说明                      |    | ---------------------------- | ----------------------- |    | Questions(2字节)(查询问题数)       | 表示查询问题区域节的数量,在请求的时候一般为1 |    | Answer RRs(2字节)(回答RR数)      | 表示回答区域的数量,根据请求一般为1      |    | Authority RRs(2字节)(权威RR数)   | 表示授权区域的数量,一般为0          |    | Additional RRs(2字节) (附加RR数) | 表示附加区域的数量一般为0           |* 正文部分(查询问题区域)    接下来就是查询的正文部分,从请求报文第16个数开始到结束都属于正文部分。       正文部分包含三个字段:`查询名(域名)`,`查询类型`和`查询类`    * 查询名           包含域名的可变长度字段,每个域以计数开头,最后一个字符为0。(也会有IP的时候,即反向查询)        ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c9710b974df64a6cac9b0313c2a68da3~tplv-k3u1fbpfcp-watermark.image?)        以上文的请求来说,从第`13`位开始        | 5 | 98 | 97 | 105 | 100 | 117 | 3 | 99 | 111 | 109 | 0 |        | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- |        | 长度 | b | a | i | d | u |长度 | c | o | m | 结束 |        比如,第一位为长度,后面5位则为`ASCII码`组成的字母,以此类推,直到标志位为0为止    * 查询类型<sup>[3](https://www.jianshu.com/p/8cdcbae986a8?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation)</sup>,占2个数,16个二进制位        | 类型  | 助记符   | 说明                |        | --- | ----- | ----------------- |        | 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   | 对所有记录的请求          |    * 查询类<sup>[3](https://www.jianshu.com/p/8cdcbae986a8?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation)</sup>,占2个数,16个位### 响应报文再来写一个转发dns请求的服务,用来获取DNS响应报文```javascriptconst dgram = require('dgram');const server = '223.5.5.5'; //需要转发的DNS服务器function forward(msg, rinfo) {  const client = dgram.createSocket('udp4');  client.on('error', (err) => {    console.log(`client error:` + err.stack);    client.close();  });  client.on('message', (fMsg, fbRinfo) => {    console.log(JSON.parse(JSON.stringify(fMsg.data))); //获取响应报文    // 转发    server.send(fMsg, rinfo.port, rinfo.address, (err) => {      err && console.log(err);    });    client.close();  });  client.send(msg, 53, fbSer, (err) => {    if (err) {      console.log(err);      client.close();    }  });}

我们再ping一下baidu.com这个域名

同样,我们把他转位二进制数据

可以看到,第三,四个数(标志位)变为129,128,对应的二进制位为10000001 10000000 上文也说到,第一个二进制位为:QR查询(Query)/响应(Response)标志,1为响应,因此可以知道他是响应报文。 其余的前半部分与请求报文一致。后面多出来的部分则为响应的数据,即

    [192,  12,   0,   1,   0,   1,   0,   0, 1,  103,  0,   4, 220, 181,  38, 148, 192,  12,   0,   1, 0,    1,  0,   0,   1, 103,   0,   4, 220, 181,  38, 251]

偏移量(2字节)

其中[192,12]是一个(2字节)指针,一般响应报文中,资源部分的地址(域名)一般都是指针C00C(1100000000001100),偏移量是12,指向请求部分的地址(域名)。

资源记录的响应类型

响应类型,也就是后面的[0,1],含义与查询问题部分的类型相同

资源记录的响应类

响应类,也就是后面的[0,1],含义与查询问题部分的类相同

生存时间(4字节)

接下去的是[0, 0, 1, 103],以秒为单位,表示的是资源记录的生命周期,可以理解为获取到的资源记录的缓存时间

资源长度

资源长度是[0, 4],ipv4是00 04

资源数据

资源数据是可变长度的字段,在这里我们拿它来指向IP地址,例如上文例子为:[220, 181, 38, 148]

后面又从[192, 0]开始,表示改域名能解析出多个IP地址

至此,DNS的请求报文与响应报文都已介绍完。

4.实现一个简单的域名解析服务

最后,让我们来做一个属于自己的DNS服务器吧~

const dgram = require('dgram');const server = dgram.createSocket('udp4');const dns = require('dns');dns.setServers(['223.5.5.5']);/** 自定义解析的域名 */let translateObj = {  'test.bbbbb.com': [220, 181, 38, 148],};/** 解析域名 */function explainDomain(dnsArr) {  let arr = [];  let queryType = []; // 查询类型  let queryClass = []; // 查询类  let len = 0;  while (dnsArr.length) {    if (dnsArr[0] === 0) {      dnsArr.splice(0, 1);      queryType = dnsArr.splice(0, 2);      queryClass = dnsArr.splice(0, 2);    } else {      if (len === 0) {        len = dnsArr.splice(0, 1);      } else {        arr = arr.concat(dnsArr.splice(0, len), [46]);        len = 0;      }    }  }  arr.pop();  return {    domain: arr.map((val) => String.fromCharCode(val)).join(''),    queryType,    queryClass,  };}/** 构造响应报文 */function createResponse(requestArr, ip) {  const response = new ArrayBuffer(requestArr.length + 16);  const resArr = [192, 12, 0, 1, 0, 1, 0, 0, 0, 218, 0, 4].concat(ip);  let bufView = new Uint8Array(response);  for (let i = 0; i < requestArr.length; i++) bufView[i] = requestArr[i];  for (let i = 0; i < resArr.length; i++) {    bufView[requestArr.length + i] = resArr[i];  }  // 将请求报文变为响应报文  bufView[2] = 129;  bufView[3] = 128;  bufView[7] = 1;  return bufView;}server.on('error', (err) => {  console.log('server error:', err.message);  server.close();});server.on('message', (msg, rinfo) => {  try {    const dnsJson = JSON.parse(JSON.stringify(msg));    const dnsData = Array.from(dnsJson.data);    let target = dnsData.splice(0, 2); // 会话标识(2字节)    let flag = dnsData.splice(0, 2); // 标志(2字节)    let data = dnsData.splice(0, 8); // 数量字段(共8字节)    let domainData = explainDomain(dnsData);    // 查找是否有自定义的域名    if (translateObj[domainData.domain]) {      /** 构造响应报文 */      const responseArr = createResponse(        dnsJson.data,        translateObj[domainData.domain]      );      server.send(responseArr, rinfo.port, rinfo.address, (err) => {        if (err) {          console.log('send error', err);          server.close();        }      });    // 没有自定义的域名,则使用其他DNS服务解析域名    } else {      dns.resolve(domainData.domain, (err, address) => {        if (err) {          console.log('dns lookup err', err);          return;        }        if (!address.length) return;        console.log(domainData.domain, address);        const responseArr = createResponse(          dnsJson.data,          address[0].split('.').map((val) => Number(val))        );        server.send(responseArr, rinfo.port, rinfo.address, (err) => {          if (err) {            console.log('send error', err);            server.close();          }        });      });    }  } catch {}});server.on('listening', () => {  const address = server.address();  console.log(`server listening at ${address.port}`);});server.bind(53);

运行服务后,我们ping一下自定义的域名test.bbbbb.com看一下是否有解析到对应的IP

小结

本文主要介绍了:

域名的组成(顶级域名,二级域名)

DNS服务架构(根域名服务器,顶级域名服务器,权威域名服务器,本地域名服务器)

DNS查询方法(递归查询,迭代查询)

DNS请求和响应报文

使用node实现一个DNS服务

参考

计算机网络原理 机械工业出版社 2018

DNS报文格式解析

NodeJS编写简单的DNS服务器

什么是域名?

原文:https://juejin.cn/post/7101952255955304484


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Node/935.html