手把手教你在netty中使用TCP协议请求DNS服务器
简介
DNS的全称domain name system,既然是一个系统就有客户端和服务器之分。一般情况来说我们并不需要感知这个DNS客户端的存在,因为我们在浏览器访问某个域名的时候,浏览器作为客户端已经实现了这个工作。
但是有时候我们没有使用浏览器,比如在netty环境中,如何构建一个DNS请求呢?
DNS传输协议简介
在RFC的规范中,DNS传输协议有很多种,如下所示:
- DNS-over-UDP/53简称"Do53",是使用UDP进行DNS查询传输的协议。
- DNS-over-TCP/53简称"Do53/TCP",是使用TCP进行DNS查询传输的协议。
- DNSCrypt,对DNS传输协议进行加密的方法。
- DNS-over-TLS简称"DoT",使用TLS进行DNS协议传输。
- DNS-over-HTTPS简称"DoH",使用HTTPS进行DNS协议传输。
- DNS-over-TOR,使用VPN或者tunnels连接DNS。
这些协议都有对应的实现方式,我们先来看下Do53/TCP,也就是使用TCP进行DNS协议传输。
DNS的IP地址
先来考虑一下如何在netty中使用Do53/TCP协议,进行DNS查询。
因为DNS是客户端和服务器的模式,我们需要做的是构建一个DNS客户端,向已知的DNS服务器端进行查询。
已知的DNS服务器地址有哪些呢?
除了13个root DNS IP地址以外,还出现了很多免费的公共DNS服务器地址,比如我们常用的阿里DNS,同时提供了IPv4/IPv6 DNS和DoT/DoH服务。
IPv4:
223.5.5.5
223.6.6.6
IPv6:
2400:3200::1
2400:3200:baba::1
DoH 地址:
https://dns.alidns.com/dns-query
DoT 地址:
dns.alidns.com
再比如百度DNS,提供了一组IPv4和IPv6的地址:
IPv4:
180.76.76.76
IPv6:
2400:da00::6666
还有114DNS:
114.114.114.114
114.114.115.115
当然还有很多其他的公共免费DNS,这里我选择使用阿里的IPv4:223.5.5.5为例。
有了IP地址,我们还需要指定netty的连接端口号,这里默认的是53。
然后就是我们要查询的域名了,这里以www.flydean.com为例。
你也可以使用你系统中配置的DNS解析地址,以mac为例,可以通过nslookup进行查看本地的DNS地址:
nslookup www.flydean.com
Server: 8.8.8.8
Address: 8.8.8.8#53
Non-authoritative answer:
www.flydean.com canonical name = flydean.com.
Name: flydean.com
Address: 47.107.98.187
Do53/TCP在netty中的使用
有了DNS Server的IP地址,接下来我们需要做的就是搭建netty client,然后向DNS server端发送DNS查询消息。
搭建DNS netty client
因为我们进行的是TCP连接,所以可以借助于netty中的NIO操作来实现,也就是说我们需要使用NioEventLoopGroup和NioSocketChannel来搭建netty客户端:
final String dnsServer = "223.5.5.5";
final int dnsPort = 53;
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new Do53ChannelInitializer());
final Channel ch = b.connect(dnsServer, dnsPort).sync().channel();
netty中的NIO Socket底层使用的就是TCP协议,所以我们只需要像常用的netty客户端服务一样构建客户端即可。
然后调用Bootstrap的connect方法连接到DNS服务器,就建立好了channel连接。
这里我们在handler中传入了自定义的Do53ChannelInitializer,我们知道handler的作用是对消息进行编码、解码和对消息进行读取。因为目前我们并不知道客户端查询的消息格式,所以Do53ChannelInitializer的实现我们在后面再进行详细讲解。
发送DNS查询消息
netty提供了DNS消息的封装,所有的DNS消息,包括查询和响应都是DnsMessage的子类。
每个DnsMessage都有一个唯一标记的ID,还有代表这个message类型的DnsOpCode。
对于DNS来说,opCode有下面这几种:
public static final DnsOpCode QUERY = new DnsOpCode(0, "QUERY");
public static final DnsOpCode IQUERY = new DnsOpCode(1, "IQUERY");
public static final DnsOpCode STATUS = new DnsOpCode(2, "STATUS");
public static final DnsOpCode NOTIFY = new DnsOpCode(4, "NOTIFY");
public static final DnsOpCode UPDATE = new DnsOpCode(5, "UPDATE");
因为每个DnsMessage都可能包含4个sections,每个section都以DnsSection来表示。因为有4个section,所以在DnsSection定义了4个section类型:
QUESTION,
ANSWER,
AUTHORITY,
ADDITIONAL;
每个section里面又包含了多个DnsRecord, DnsRecord代表的就是Resource record,简称为RR,RR中有一个CLASS字段,下面是DnsRecord中CLASS字段的定义:
int CLASS_IN = 1;
int CLASS_CSNET = 2;
int CLASS_CHAOS = 3;
int CLASS_HESIOD = 4;
int CLASS_NONE = 254;
int CLASS_ANY = 255;
DnsMessage是DNS消息的统一表示,对于查询来说,netty中提供了一个专门的查询类叫做DefaultDnsQuery。
先来看下DefaultDnsQuery的定义和构造函数:
public class DefaultDnsQuery extends AbstractDnsMessage implements DnsQuery {
public DefaultDnsQuery(int id) {
super(id);
}
public DefaultDnsQuery(int id, DnsOpCode opCode) {
super(id, opCode);
}
DefaultDnsQuery的构造函数需要传入id和opCode。
我们可以这样定义一个DNS查询:
int randomID = (int) (System.currentTimeMillis() / 1000);
DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY)
既然是QEURY,那么还需要设置4个sections中的查询section:
query.setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A));
这里调用的是setRecord方法向section中插入RR数据。
这里的RR数据使用的是DefaultDnsQuestion。DefaultDnsQuestion的构造函数有两个,一个是要查询的domain name,这里就是"www.flydean.com",另外一个参数是dns记录的类型。
dns记录的类型有很多种,在netty中有一个专门的类DnsRecordType表示,DnsRecordType中定义了很多个类型,如下所示:
public class DnsRecordType implements Comparable<DnsRecordType> {
public static final DnsRecordType A = new DnsRecordType(1, "A");
public static final DnsRecordType NS = new DnsRecordType(2, "NS");
public static final DnsRecordType CNAME = new DnsRecordType(5, "CNAME");
public static final DnsRecordType SOA = new DnsRecordType(6, "SOA");
public static final DnsRecordType PTR = new DnsRecordType(12, "PTR");
public static final DnsRecordType MX = new DnsRecordType(15, "MX");
public static final DnsRecordType TXT = new DnsRecordType(16, "TXT");
...
因为类型比较多,我们挑选几个常用的进行讲解。
- A类型,是address的缩写,用来指定主机名或者域名对应的ip地址.
- NS类型,是name server的缩写,是域名服务器记录,用来指定域名由哪个DNS服务器来进行解析。
- MX类型,是mail exchanger的缩写,是一个邮件交换记录,用来根据邮箱的后缀来定位邮件服务器。
- CNAME类型,是canonical name的缩写,可以将多个名字映射到同一个主机.
- TXT类型,用来表示主机或者域名的说明信息。
以上几个是我们经常会用到的dns record类型。
这里我们选择使用A,用来查询域名对应的主机IP地址。
构建好query之后,我们就可以使用netty client发送query指令到dns服务器了,具体的代码如下:
DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY)
.setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A));
ch.writeAndFlush(query).sync();
DNS查询的消息处理
DNS的查询消息我们已经发送出去了,接下来就是对消息的处理和解析了。
还记得我们自定义的Do53ChannelInitializer吗?看一下它的实现:
class Do53ChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
p.addLast(new TcpDnsQueryEncoder())
.addLast(new TcpDnsResponseDecoder())
.addLast(new Do53ChannelInboundHandler());
}
}
我们向pipline中添加了两个netty自带的编码解码器TcpDnsQueryEncoder和TcpDnsResponseDecoder,还有一个自定义用来做消息解析的Do53ChannelInboundHandler。
因为我们向channel中写入的是DnsQuery,所以需要一个encoder将DnsQuery编码为ByteBuf,这里使用的是netty提供的TcpDnsQueryEncoder:
public final class TcpDnsQueryEncoder extends MessageToByteEncoder<DnsQuery>
TcpDnsQueryEncoder继承自MessageToByteEncoder,表示将DnsQuery编码为ByteBuf。
看下他的encode方法:
protected void encode(ChannelHandlerContext ctx, DnsQuery msg, ByteBuf out) throws Exception {
out.writerIndex(out.writerIndex() + 2);
this.encoder.encode(msg, out);
out.setShort(0, out.readableBytes() - 2);
}
可以看到TcpDnsQueryEncoder在msg编码之前存储了msg的长度信息,所以是一个基于长度的对象编码器。
这里的encoder是一个DnsQueryEncoder对象。
看一下它的encoder方法:
void encode(DnsQuery query, ByteBuf out) throws Exception {
encodeHeader(query, out);
this.encodeQuestions(query, out);
this.encodeRecords(query, DnsSection.ADDITIONAL, out);
}
DnsQueryEncoder会依次编码header、questions和records。
完成编码之后,我们还需要从DNS server的返回中decode出DnsResponse,这里使用的是netty自带的TcpDnsResponseDecoder:
public final class TcpDnsResponseDecoder extends LengthFieldBasedFrameDecoder
TcpDnsResponseDecoder继承自LengthFieldBasedFrameDecoder,表示数据是以字段长度来进行分割的,这和我们刚刚将的encoder的格式类似。
来看下他的decode方法:
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf frame = (ByteBuf)super.decode(ctx, in);
if (frame == null) {
return null;
} else {
DnsResponse var4;
try {
var4 = this.responseDecoder.decode(ctx.channel().remoteAddress(), ctx.channel().localAddress(), frame.slice());
} finally {
frame.release();
}
return var4;
}
}
decode方法先调用LengthFieldBasedFrameDecoder的decode方法将要解码的内容提取出来,然后调用responseDecoder的decode方法,最终返回DnsResponse。
这里的responseDecoder是一个DnsResponseDecoder。具体decoder的细节这里就不过多阐述了。感兴趣的同学可以自行查阅代码文档。
最后,我们得到了DnsResponse对象。
接下来就是自定义的InboundHandler对消息进行解析了:
class Do53ChannelInboundHandler extends SimpleChannelInboundHandler<DefaultDnsResponse>
在它的channelRead0方法中,我们调用了readMsg方法对消息进行处理:
private static void readMsg(DefaultDnsResponse msg) {
if (msg.count(DnsSection.QUESTION) > 0) {
DnsQuestion question = msg.recordAt(DnsSection.QUESTION, 0);
log.info("question is :{}",question);
}
int i = 0, count = msg.count(DnsSection.ANSWER);
while (i < count) {
DnsRecord record = msg.recordAt(DnsSection.ANSWER, i);
//A记录用来指定主机名或者域名对应的IP地址
if (record.type() == DnsRecordType.A) {
DnsRawRecord raw = (DnsRawRecord) record;
log.info("ip address is: {}",NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content())));
}
i++;
}
}
DefaultDnsResponse是DnsResponse的一个实现,首先判断msg中的QUESTION个数是否大于零。
如果大于零,则打印出question的信息。
然后再解析出msg中的ANSWER并打印出来。
最后,我们可能得到这样的输出:
INFO c.f.dnstcp.Do53ChannelInboundHandler - question is :DefaultDnsQuestion(www.flydean.com. IN A)
INFO c.f.dnstcp.Do53ChannelInboundHandler - ip address is: 47.107.98.187
总结
以上就是使用netty创建DNS client进行TCP查询的讲解。
本文的代码,大家可以参考:
更多内容请参考 http://www.flydean.com/54-netty-dns-over-tcp/
最通俗的解读,最深刻的干货,最简洁的教程,众多你不
欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!
手把手教你在netty中使用TCP协议请求DNS服务器的更多相关文章
- netty系列之: 在netty中使用 tls 协议请求 DNS 服务器
目录 简介 支持DoT的DNS服务器 搭建支持DoT的netty客户端 TLS的客户端请求 总结 简介 在前面的文章中我们讲过了如何在netty中构造客户端分别使用tcp和udp协议向DNS服务器请求 ...
- netty系列之:来,手把手教你使用netty搭建一个DNS tcp服务器
目录 简介 搭建netty服务器 DNS服务器的消息处理 DNS客户端消息请求 总结 简介 在前面的文章中,我们提到了使用netty构建tcp和udp的客户端向已经公布的DNS服务器进行域名请求服务. ...
- 手把手教你写Sublime中的Snippet
手把手教你写Sublime中的Snippet Sublime Text号称最性感的编辑器, 并且越来越多人使用, 美观, 高效 关于如何使用Sublime text可以参考我的另一篇文章, 相信你会喜 ...
- 卫星网络中使用TCP协议的劣势(所以才有TCP优化版用来卫星通信啊,比如TCP-Peach和ADolar)
卫星网络中使用TCP协议的劣势 为了避免产生网络拥塞,原TCP协议综合采用了慢启动.拥塞避免.快速重传以及快速恢复等算法.但这些算法应用的前提是网络发生拥塞造成丢包,然而在误码率相对较高的卫星通信系统 ...
- Python中的Tcp协议的应用之Tcp服务端程序开发
TCP通信协议是面向连接的可靠的网络通信协议. 网络间想要进行数据传输必须要用到socket,socket翻译过来叫做套接字,其主要作用是不同设备或同一台设备之间的进程通信工具. Python中的Tc ...
- 手把手教你基于Netty实现一个基础的RPC框架(通俗易懂)
阅读这篇文章之前,建议先阅读和这篇文章关联的内容. [1]详细剖析分布式微服务架构下网络通信的底层实现原理(图解) [2][年薪60W的技巧]工作了5年,你真的理解Netty以及为什么要用吗?(深度干 ...
- 快来体验快速通道,netty中epoll传输协议详解
目录 简介 epoll的详细使用 EpollEventLoopGroup EpollEventLoop EpollServerSocketChannel EpollSocketChannel 总结 简 ...
- 面试中的TCP协议
TCP的三次握手和四次挥手 三次握手 TCP连接是通过三次握手来连接的. 第一次握手 当客户端向服务器发起连接请求时,客户端会发送同步序列标号SYN到服务器,在这里我们设SYN为x,等待服务器确认,这 ...
- 转 gSOAP中使用TCP协议传输数据
一 模型 TCP/IP是一个协议族(Internet protocol suite),包含众多的协议,传输控制协议(TCP)和网际协议(IP)分属不同的层次,是保证数据完整传输的两个基本的重要协议. ...
随机推荐
- clion 预编译文件的查看
看了一圈网上也没有我能一下就能看的懂的配置教程 我就手打一篇给在用clion的同学来参考一下 本文适用于g++编译 cmake Ninja生成器 clion 默认使用的是CMAKE来构建程序 生成器用 ...
- Hadoop(三)通过C#/python实现Hadoop MapReduce
MapReduce Hadoop中将数据切分成块存在HDFS不同的DataNode中,如果想汇总,按照常规想法就是,移动数据到统计程序:先把数据读取到一个程序中,再进行汇总. 但是HDFS存的数据量非 ...
- ONNXRuntime学习笔记(二)
继上一篇计划的实践项目,这篇记录我训练模型相关的工作. 首先要确定总体目标:训练一个pytorch模型,CIFAR-100数据集测试集acc达到90%:部署后推理效率达到50ms/张, 部署平台为wi ...
- logging、openpyxl、第三方模块下载
### 日志模块的组成部分 ```pythonimport logging# 1.logger对象:产生日志logger = logging.getLogger('转账记录')# 2.filter对象 ...
- CTF简介
最近在学习渗透测试,后来发现CTF很有趣,发现对学习有所帮助,于是找了几个网站,下面推荐几个我觉得不错的网站 https://www.ctfhub.com/#/index https://adworl ...
- AC自动机:Tire树+KMP
简介 AC自动机是一个多模式匹配算法,在模式匹配领域被广泛应用,举一个经典的例子,违禁词查找并替换为***.AC自动机其实是Trie树和KMP 算法的结合,首先将多模式串建立一个Tire树,然后结合K ...
- arts-week10
Algorithm 905. Sort Array By Parity - LeetCode Review Who's Afraid of the Big Bad Preloader? 一文读懂前端缓 ...
- 一条更新SQL的内部执行及日志模块
一条更新SQL的内部执行 学习MySQL实战45讲,非常推荐学 还是老图: 上文复习 在执行查询语句的时候,会执行连接器(总要连上才能搞事情),然后去查询缓存(MySQL8+删除了),有数据返回,没数 ...
- 使用 Postman 实现 API 自动化测试
背景介绍 相信大部分开发人员和测试人员对 postman 都十分熟悉,对于开发人员和测试人员而言,使用 postman 来编写和保存测试用例会是一种比较方便和熟悉的方式.但 postman 本身是一个 ...
- Python工程:ImportError: attempted relative import with no known parent package
Python工程:ImportError: attempted relative import with no known parent package 解决方法: 1.对每个目录创建的时候都选择创建 ...