赞
踩
本节,我们基于上一节理论的基础上,用代码实现DNS数据包的发送和解析。这里有两点需要重复,一是我们将使用DNS的递归式传输模式,也就是消息的发送如下图:
也就是我们将在数据包中的特定数据段内设置标志位,要求第一台域名解析服务器帮我们实现所有的查询流程,然后把最终结果返回给我们,这样我们可以省却多种数据交互和解析流程,一般而言第一台域名解析服务器都是路由器。
第二个值得我们了解的要点是DNS数据包的基本格式:
它包括固定的头部,以及相应的消息体部分。由于头部内容固定不变,因此我们可以在代码实现中写死,它的基本组成结构如下:
重要的是有两个可变的数据部分需要我们掌握,一个是Question数据格式,它包含了客户端向服务器请求的内容格式,它的组成如下:
当我们想要解析某个域名对应的IP时,我们需要按照上面的结构组织信息发布给服务器,服务器顺利解读后会给我们发送如下格式的应答信息:
由此我们代码的目的是构造包头,然后将要查询的域名信息按照上面给出的Question数据格式组织好发送给路由器并等待其回复,拿到回复数据包之后,我们再按照上头anwser resource格式解析服务器返回的数据。
接下来让我们看看代码实现:
package Application; import java.nio.ByteBuffer; import java.util.Random; public class DNSApplication extends Application { private byte[] resove_server_ip = null; private String domainName = ""; private byte[] dnsHeader = null; private int transition_id = 0; public DNSApplication( byte[] destIP, String domainName) { this.resove_server_ip = destIP; this.domainName = domainName; Random rand = new Random(); transition_id = rand.nextInt(); constructDNSPacketHeader(); } private void constructDNSPacketHeader() { /* * 构造DNS数据包包头,总共12字节 */ byte[] header = new byte[12]; ByteBuffer buffer = ByteBuffer.wrap(header); //2字节的会话id buffer.putShort((short)transition_id); //接下来是2字节的操作码,不同的比特位有相应含义 short opCode = 0; /* * 如果是查询数据包,第0个比特位要将最低位设置为0,接下来的4个比特位表示查询类型,如果是查询ip则设置为0, * 第5个比特位由服务器在回复数据包中设置,用于表明信息是它拥有的还是从其他服务器查询而来, * 第6个比特位表示消息是否有分割,有的话设置为1,由于我们使用UDP,因此消息不会有分割。 * 第7个比特位表示是否使用递归式查询请求,我们设置成1表示使用递归式查询, * 第8个比特位由服务器返回时设置,表示它是否接受递归式查询 * 第9,10,11,3个比特位必须保留为0, * 最后四个比特由服务器回复数据包设置,0表示正常返回数据,1表示请求数据格式错误,2表示服务器出问题,3表示不存在给定域名等等 * 我们发送数据包时只要将第7个比特位设置成1即可 */ opCode = (short) (opCode | (1 << 7)); buffer.putShort(opCode); //接下来是2字节的question count,由于我们只有1个请求,因此它设置成1 short questionCount = 1; buffer.putShort(questionCount); //剩下的默认设置成0 short answerRRCount = 0; buffer.putShort(answerRRCount); short authorityRRCount = 0; buffer.putShort(authorityRRCount); short additionalRRCount = 0; buffer.putShort(additionalRRCount); this.dnsHeader = buffer.array(); } }
上面代码中,函数constructDNSPacketHeader完成了查询数据包头部数据的组装,接下来我们我们实现Question数据部分的组装:
private void constructDNSPacketQuestion() { /* * 构造DNS数据包中包含域名的查询数据结构 * 首先是要查询的域名,它的结构是是:字符个数+是对应字符, * 例如域名字符串pan.baidu.com对应的内容为 * 3pan[5]baidu[3]com也就是把‘.'换成它后面跟着的字母个数 */ //根据.将域名分割成多个部分,第一个1用于记录"pan"的长度,第二个1用0表示字符串结束 dnsQuestion = new byte[1 + 1 + domainName.length() + QUESTION_TYPE_LENGTH + QUESTION_CLASS_LENGTH]; String[] domainParts = domainName.split("\\."); ByteBuffer buffer = ByteBuffer.wrap(dnsQuestion); for (int i = 0; i < domainParts.length; i++) { //先填写字符个数 buffer.put((byte)domainParts[i].length()); //填写字符 for(int k = 0; k < domainParts[i].length(); k++) { buffer.put((byte) domainParts[i].charAt(k)); } } //表示域名字符串结束 byte end = 0; buffer.put(end); //填写查询问题的类型和级别 buffer.putShort(QUESTION_TYPE_A); buffer.putShort(QUESTION_CLASS); }
上面代码根据我们前面描述的Question数据结构,将要查询的域名字符串封装起来发送给服务器进行解析。完成两部分关键数据的组装后,我们就可以将其组合成一个完整的DNS数据包发送出去:
public void queryDomain() { //向服务器发送域名查询请求数据包 byte[] dnsPacketBuffer = new byte[dnsHeader.length + dnsQuestion.length]; ByteBuffer buffer = ByteBuffer.wrap(dnsPacketBuffer); buffer.put(dnsHeader); buffer.put(dnsQuestion); byte[] udpHeader = createUDPHeader(dnsPacketBuffer); byte[] ipHeader = createIP4Header(udpHeader.length); byte[] dnsPacket = new byte[udpHeader.length + ipHeader.length]; buffer = ByteBuffer.wrap(dnsPacket); buffer.put(ipHeader); buffer.put(udpHeader); //将消息发送给路由器 try { ProtocolManager.getInstance().sendData(dnsPacket, resove_server_ip); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } }
我们在前面章节中已经多次展示过UDP包头和IP包头的组装,在这里我们不再将其代码罗列出来,当上面代码完成后执行时,我们通过wireshark监控可以发现,程序顺利构造了对应的DNS数据包发送,同时受到了服务器的回复数据包:
接下来我们要做的是解析服务器回复的数据包,解析代码如下:
public void handleData(HashMap<String, Object> headerInfo) { /* * 解读服务器回发的数据包,首先读取头2字节判断transition_id是否与我们发送时使用的一致 */ byte[] data = (byte[])headerInfo.get("data"); if (data == null) { System.out.println("empty data"); return; } ByteBuffer buffer = ByteBuffer.wrap(data); short transionID = buffer.getShort(); if (transionID != this.transition_id) { System.out.println("transition id different!"); return; } //读取2字节flag各个比特位的含义 short flag = buffer.getShort(); readFlags(flag); //接下来2字节表示请求的数量 short questionCount = buffer.getShort(); System.out.println("client send " + questionCount + " requests"); //接下来的2字节表示服务器回复信息的数量 short answerCount = buffer.getShort(); System.out.println("server return " + answerCount + " answers"); //接下来2字节表示数据拥有属性信息的数量 short authorityCount = buffer.getShort(); System.out.println("server return " + authorityCount + " authority resources"); //接下来2字节表示附加信息的数量 short additionalInfoCount = buffer.getShort(); System.out.println("serve return " + additionalInfoCount + " additional infos"); //回复数据包会将请求数据原封不动的复制,所以接下来我们先处理question数据结构 readQuestions(questionCount, buffer); //读取服务器回复信息 readAnswers(answerCount, buffer); }
一旦接收到服务器回发的数据包,上面函数就会被调用,首先它解析包头,看看会话id是否与我们发出数据包的id相匹配,然后读取余下内容。由于服务器回复的数据中包含了请求数据包发送的Question数据部分,因此我们也进行相应解读:
private void readQuestions(int count, ByteBuffer buffer) { for (int i = 0; i < count; i++) { readStringContent(buffer); //查询问题的类型 short questionType = buffer.getShort(); if (questionType == QUESTION_TYPE_A) { System.out.println("request ip for given domain name"); } //查询问题的级别 short questionClass = buffer.getShort(); System.out.println("the class of the request is " + questionClass); } } private void readStringContent(ByteBuffer buffer) { byte charCnt = buffer.get(); while(charCnt > 0) { //输出字符 for (int i = 0; i < charCnt; i++) { System.out.print((char)buffer.get()); } charCnt = buffer.get(); if (charCnt != 0) { System.out.print("."); } } System.out.println("\n"); }
这里需要注意的是解析域名字符串的格式,它是[数字][字符]格式,因此代码读取时首先获得字符的个数,然后再读取相应字符。接下来我们看看读取Answer Resource Record的代码实现,该数据结构的解析稍微复杂一些:
private void readAnswers(int count, ByteBuffer buffer) { /* * 回复信息的格式如下: * 第一个字段是name,它的格式如同请求数据中的域名字符串 * 第二个字段是类型,2字节 * 第三字段是级别,2字节 * 第4个字段是Time to live, 4字节,表示该信息可以缓存多久 * 第5个字段是数据内容长度,2字节 * 第6个字段是内如数组,长度如同第5个字段所示 */ /* * 在读取第name字段时,要注意它是否使用了压缩方式,如果是那么该字段的第一个字节就一定大于等于192,也就是 * 它会把第一个字节的最高2比特设置成11,接下来的1字节表示数据在dns数据段中的偏移 */ for (int i = 0; i < count; i++) { System.out.println("Name content in answer filed is: "); if (isNameCompression(buffer.get())) { int offset = (int)buffer.get(); byte[] array = buffer.array(); ByteBuffer dup_buffer = ByteBuffer.wrap(array); //从指定偏移处读取字符串内容 dup_buffer.position(offset); readStringContent(dup_buffer); } else { readStringContent(buffer); } short type = buffer.getShort(); System.out.println("answer type is : " + type); //接下来2字节对应type if (type == DNS_ANSWER_CANONICAL_NAME_FOR_ALIAS) { System.out.println("this answer contains server string name"); } //接下来2字节是级别 short cls = buffer.getShort(); System.out.println("answer class: " + cls); //接下来4字节是time to live int ttl = buffer.getInt(); System.out.println("this information can cache " + ttl + " seconds"); //接下来2字节表示数据长度 short rdLength = buffer.getShort(); System.out.println("content length is " + rdLength); if (type == DNS_ANSWER_CANONICAL_NAME_FOR_ALIAS) { readStringContent(buffer); } if (type == DNS_ANSWER_HOST_ADDRESS) { //显示服务器返回的IP byte[] ip = new byte[4]; for (int k = 0; k < 4; k++) { ip[k] = buffer.get(); } try { InetAddress ipAddr = InetAddress.getByAddress(ip); System.out.println("ip address for domain name is: " + ipAddr.getHostAddress()); } catch (UnknownHostException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //if (type == DNS_ANSWER_HOST_ADDRESS) } //for (int i = 0; i < count; i++) } private boolean isNameCompression(byte b) { if ((b & (1<<7)) != 0 && (b & (1<<6)) != 0) { return true; } return false; } private void readFlags(short flag) { //最高字节为1表示该数据包为回复数据包 if ((flag & (1 << 15))!= 0) { System.out.println("this is packet return from server"); } //如果第9个比特位为1表示客户端请求递归式查询 if ((flag & (1 << 8)) != 0) { System.out.println("client requests recursive query!"); } //第8个比特位为1表示服务器接受递归式查询请求 if ((flag & (1 << 7)) != 0) { System.out.println("server accept recursive query request!"); } //第6个比特位表示服务器是否拥有解析信息 if ((flag & (1 << 5)) != 0) { System.out.println("sever own the domain info"); } else { System.out.println("server query domain info from other servers"); } }
这里需要特别注意的一点是,在服务器返回的应答数据中,它会对字符串进行压缩,我们看下图:
我在上头选择字符串pan.baidu.com,但下面只对应两个字节。这是因为回复的数据包为了节省内容长度,如果字符串在数据包的前面出现过,那么它就不会再把相同的数据重复一遍。它会用两个字节表示重复信息,第一个字节的最高两个比特设置成11,表示当前字符串使用压缩表示法,根据上一节描述,当解析数据包字符串时,我们首先读取的是字符个数,如果不采用压缩表示,那么字符个数不允许超过63,因此该字节的头两位绝不可能是11.
如果字节头两位是11,那么我们就确认数据包使用了字符串压缩法。第二个字节告诉我们字符串所在位置相对偏移。例如上图中第二个字节0c表示从DNS数据开始偏移12个字节就是本处要显示的字符串。所以在函数readAnswer的实现中,在读取字符串时,如果发现采用压缩方法,那么它就读取第二个字节获得偏移,然后从数据段起始处偏移相应字节后再进行读取。
更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
新书上架,请诸位朋友多多支持:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。