当前位置: 首页 > 后端技术 > Java

手把手教你在netty中使用TCP协议请求DNS服务器

时间:2023-04-02 01:49:33 Java

教你在netty中使用TCP协议请求DNS服务器一般情况下,我们不需要知道这个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或隧道连接到DNS。这些协议都有相应的实现方法。我们先来看Do53/TCP,它使用TCP进行DNS协议传输。DNS的IP地址我们先来考虑如何在netty中使用Do53/TCP协议进行DNS查询。因为DNS是客户端-服务器模型,所以我们需要做的就是搭建一个DNS客户端来查询已知的DNS服务器。什么是已知的DNS服务器地址?除了13根DNSIP地址外,还有很多免费的公共DNS服务器地址,比如我们常用的阿里DNS,它同时提供IPv4/IPv6DNS和DoT/DoH服务。IPv4:223.5.5.5223.6.6.6IPv6:2400:3200::12400:3200:baba::1DoH地址:https://dns.alidns.com/dns-queryDoT地址:dns.alidns.com再比如百度DNS,提供了一组IPv4和IPv6地址:IPv4:180.76.76.76IPv6:2400:da00::6666和114DNS:114.114.114.114114.114.115.115当然还有很多其他的公共免费DNS,这里我选择使用阿里的Take以IPv4:223.5.5.5为例。有了IP地址,我们还需要指定netty的连接端口号,这里默认是53。然后就是我们要查询的域名,这里以www.flydean.com为例。您也可以使用您系统中配置的DNS解析地址。以mac为例,可以通过nslookup查看本地DNS地址:nslookupwww.flydean.comServer:8.8.8.8Address:8.8.8.8#53非权威回答:www.flydean.comcanonicalname=flydean.com.Name:flydean.com地址:47.107.98.187Do53/TCP在netty中的使用有了DNSServer的IP地址,接下来我们要做的就是搭建nettyclient,然后向DNS服务器发送DNS查询报文。搭建一个DNSnetty客户端,因为我们是做一个TCP连接,所以可以借助netty中的NIO操作来实现,也就是说,我们需要使用NioEventLoopGroup和NioSocketChannel来搭建一个netty客户端:finalStringdnsServer="223.5.5.5";finalintdnsPort=53;EventLoopGroupgroup=newNioEventLoopGroup();Bootstrapb=newBootstrap();b.group(group).channel(NioSocketChannel.class).handler(newDo53ChannelInitializer());最终通道ch=b.connect(dnsServer,dnsPort).sync().channel();netty中的NIOSocket底层使用的是TCP协议,所以我们只需要像普通的netty客户端服务一样搭建客户端即可。然后调用Bootstrap的connect方法连接DNS服务器,通道连接建立。这里我们在handler中传入了自定义的Do53ChannelInitializer。我们知道处理程序的作用是对消息进行编码、解码和读取。因为我们目前不知道客户端查询的消息格式,后面会详细讲解Do53ChannelInitializer的实现。发送DNS查询消息netty提供了对DNS消息的封装,所有的DNS消息,包括查询和响应,都是DnsMessage的子类。每个DnsMessage都有一个唯一标记的ID和一个表示消息类型的DnsOpCode。对于DNS,opCode有以下类型:publicstaticfinalDnsOpCodeQUERY=newDnsOpCode(0,"QUERY");publicstaticfinalDnsOpCodeIQUERY=newDnsOpCode(1,"IQUERY");publicstaticfinalDnsOpCodeSTATUS=newDnsOpCode(2,"STATUS");publicstaticfinalDnsOpCodeNOTIFY=newDnsOpCode(4,"NOTIFY");publicstaticfinalDnsOpCodeUPDATE=newDnsOpCode(5,"UPDATE");因为每个DnsMessage可能包含4个Section,每个section用DnsSection表示。因为有4个section,所以在DnsSection中定义了4种sectiontype:QUESTION,ANSWER,AUTHORITY,ADDITIONAL;每个section包含多个DnsRecords,DnsRecord表示Resourcerecord,简称RR,RR中有一个CLASS字段,下面是DnsRecord中CLASS字段的定义:intCLASS_IN=1;intCLASS_CSNET=2;intCLASS_CHAOS=3;intCLASS_HESIOD=4;intCLASS_NONE=254;intCLASS_ANY=255;对于查询,netty提供了一个特殊的查询类,叫做DefaultDnsQuery。首先看一下DefaultDnsQuery的定义和构造函数:}publicDefaultDnsQuery(intid,DnsOpCodeopCode){super(id,opCode);DefaultDnsQuery的构造函数需要传入id和opCode。我们可以这样定义DNS查询:intrandomID=(int)(System.currentTimeMillis()/1000);DnsQueryquery=newDefaultDnsQuery(randomID,DnsOpCode.QUERY)由于是QEURY,需要在4段section中设置查询:query.setRecord(DnsSection.QUESTION,newDefaultDnsQuestion(queryDomain,DnsRecordType.A));这里调用setRecord方法将RR数据插入到section中。这里的RR数据使用的是DefaultDnsQuestion。DefaultDnsQuestion的构造函数有两个,一个是要查询的域名,这里是“www.flydean.com”,另一个参数是dns记录的类型。dns记录有很多种。netty中有一个特殊的类DnsRecordType。DnsRecordType中定义了很多类型,如下所示:publicclassDnsRecordTypeimplementsComparable{publicstaticfinalDnsRecordTypeA=newDnsRecordType(1,"A");publicstaticfinalDnsRecordTypeNS=newDnsRecordType(2,"NS");publicstaticfinalDnsRecordTypeCNAME=newDnsRecordType(5,"CNAME");publicstaticfinalDnsRecordTypeSOA=newDnsRecordType(6,"SOA");publicstaticfinalDnsRecordTypePTR=newDnsRecordType(12,"PTR");publicstaticfinalDnsRecordTypeMX=newDnsRecordType(15,"MX");publicstaticfinalDnsRecordTypeTXT=newDnsRecordType(16,"TXT");……因为种类比较多,我们就挑几个常用的来讲解一下。type是address的缩写,用来指定主机名或域名对应的ip地址。NS类型,是nameserver的缩写,即域名服务器记录,用于指定该域名由哪台DNS服务器解析。MX类型,是mailexchanger的缩写,是一种邮件交换记录,用于根据邮箱的后缀定位邮件服务器。CNAME类型是canonicalname的缩写,可以将多个名字映射到同一个主机上。TXT类型用于表示主机或域名的描述信息。以上就是我们经常使用的dns记录类型。这里我们选择使用A来查询域名对应的主机IP地址。构建好查询后,我们就可以使用netty客户端向dns服务器发送查询命令了。具体代码如下:DnsQueryquery=newDefaultDnsQuery(randomID,DnsOpCode.QUERY).setRecord(DnsSection.QUESTION,newDefaultDnsQuestion(queryDomain,DnsRecordType.A));ch.writeAndFlush(查询).sync();DNS查询报文我们已经发送了DNS查询报文,接下来就是对报文进行处理和分析了。还记得我们自定义的Do53ChannelInitializer吗?看一下它的实现:p.addLast(newTcpDnsQueryEncoder()).addLast(newTcpDnsResponse())seDecode.addLast(newDo53ChannelInboundHandler());我们在管道中添加了两个编解码器TcpDnsQueryEncoder和TcpDnsResponseDecoder,以及用于消息解析的自定义Do53ChannelInboundHandler。因为我们写入通道的是DnsQuery,所以需要一个encoder将DnsQuery编码成ByteBuf。这里使用netty提供的TcpDnsQueryEncoder:publicfinalclassTcpDnsQueryEncoderextendsMessageToByteEncoderTcpDnsQueryEncoder继承自MessageToByteEncoder,表示DnsQueryEncoder编码为.看他的encode方法:protectedvoidencode(ChannelHandlerContextctx,DnsQuerymsg,ByteBufout)throwsException{out.writerIndex(out.writerIndex()+2);this.encoder.encode(msg,out);out.setShort(0,out.readableBytes()-2);可以看到TcpDnsQueryEncoder在msg编码之前存储了msg的长度信息,所以它是一个基于长度的对象编码器。这里的编码器是一个DnsQueryEncoder对象。看看它的编码器方法:voidencode(DnsQueryquery,ByteBufout)throwsException{encodeHeader(query,out);this.encodeQuestions(查询,输出);this.encodeRecords(查询,DnsSection.ADDITIONAL,输出);}DnsQueryEncoderheader、问题和记录将按顺序编码。编码完成后,我们还需要对DNS服务器返回的DnsResponse进行解码。这里我们使用netty自带的TcpDnsResponseDecoder:publicfinalclassTcpDnsResponseDecoderextendsLengthFieldBasedFrameDecoderTcpDnsResponseDecoder继承自LengthFieldBasedFrameDecoder,表示数据按字段长度划分,这和我们刚才改的encoder的格式类似。我们看一下他的decode方法:如果(框架==空){返回空;}else{DnsResponsevar4;try{var4=this.responseDecoder.decode(ctx.channel().remoteAddress(),ctx.channel().localAddress(),frame.slice());}最后{frame.release();}返回var4;decode方法首先调用LengthFieldBasedFrameDecoder的decode方法提取出待解码的内容,然后调用responseDecoder的decode方法,最后返回DnsResponse。这里的responseDecoder是一个DnsResponseDecoder。具体解码器的细节这里不再赘述。感兴趣的同学可以自行查阅代码文档。最后,我们得到DnsResponse对象。接下来是自定义的InboundHandler解析消息:classDo53ChannelInboundHandlerextendsSimpleChannelInboundHandler在它的channelRead0方法中,我们调用readMsg方法来处理消息:privatestaticvoidreadMsg(DefaultDnsResponsemsg){if(msg.count(DnsSection.QUESTION))>0){DnsQuestion问题=msg.recordAt(DnsSection.QUESTION,0);log.info("问题是:{}",question);}inti=0,count=msg.count(DnsSection.ANSWER);while(i