简介
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.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,这里我抉择应用阿里的IPv4:223.5.5.5为例。
有了IP地址,咱们还须要指定netty的连贯端口号,这里默认的是53。
而后就是咱们要查问的域名了,这里以www.flydean.com为例。
你也能够应用你零碎中配置的DNS解析地址,以mac为例,能够通过nslookup进行查看本地的DNS地址:
nslookup www.flydean.comServer: 8.8.8.8Address: 8.8.8.8#53Non-authoritative answer:www.flydean.com canonical name = flydean.com.Name: flydean.comAddress: 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查问的解说。
本文的代码,大家能够参考:
learn-netty4
更多内容请参考 http://www.flydean.com/54-netty-dns-over-tcp/
最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不
欢送关注我的公众号:「程序那些事」,懂技术,更懂你!