Dns(Domain Name Server)即域名服务器,在网络中承担着将域名转换为ip地址的工作。在很多编程中都要用到这种技术,就是使用域名解析。这篇文章将说明这项技术。

通过Dns服务器,可以查询很多地址,比如mail服务器地址,ftp服务器等等,我在这里就以mail服务器为例,并以java实现。

+---------------------+
  |    Header    |
  +---------------------+
  |    Question   | 
  +---------------------+
  |    Answer    | 
  +---------------------+
  |   Authority   | 
  +---------------------+
  |   Additional   | 
  +---------------------+
这个表是从rfc1035文档中拷出来的,大致说明了dns包的格式。
Header 
   0 1 2 3 4 5 6 7 8 9 A B C D E F
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |           ID            |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |QR|  Opcode |AA|TC|RD|RA|  Z  |  RCODE  |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |          QDCOUNT          |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |          ANCOUNT          |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |          NSCOUNT          |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |          ARCOUNT          |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    这个也是从rfc文档中拷出来的,只是我将其头部数字改成16进制了。
 
ID: 16位的一个标志,用以验证请求和回复消息的匹配。就实用程序产生一个16位的随机数。
QR: 1位数据表明这是一个请求,还是一个回复(0为请求,1为恢复)。
Opcode: 4位的数据表示查询的类型。
0       基本查找
1       反向查找
2       查询服务器情况
3-15    保留
RD:(recursion desired)即是否以递归方式的查询,RD=1为递归。
RA:(Recursion Available)表示服务器是否支持递归方式查询,只在回复中有效。
QDCOUNT:16位数据表示要查询的问题个数。
ANCOUNT:16位数据表示回复结果的个数,只在回复中有效。
 
其他几个请参考rfc文档,在这里我们只用这些,并将其他参数设为0。
 
Question
   0 1 2 3 4 5 6 7 8 9 A B C D E F
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |                        |
  /           QNAME           /
  /                        /
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |           QTYPE           |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |           QCLASS          |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 
QNAME: 要求查询的域名地址。比如有这样一个邮件地址XXX@163.net,
  我们将@后面的地址提取出来,即163.net。然后将其变为这样一个序列,0316303net0,也就是以 . 分界,并以各自的字符个数作为前缀,最后以0结束
QTYPE: 2位数据表示查询类型。
    A        1 a host address
NS       2 an authoritative name server
MD       3 a mail destination (Obsolete - use MX)
MF       4 a mail forwarder (Obsolete - use MX)
CNAME      5 the canonical name for an alias
SOA       6 marks the start of a zone of authority
MB       7 a mailbox domain name (EXPERIMENTAL
MG       8 a mail group member (EXPERIMENTAL)
MR       9 a mail rename domain name (EXPERIMENTAL)
NULL      10 a null RR (EXPERIMENTAL)
WKS       11 a well known service description
PTR       12 a domain name pointer
HINFO      13 host information
MINFO      14 mailbox or mail list information
MX       15 mail exchange
TXT       16 text strings
 
这是在rfc文档中列出的各类type,我们在这里用MX,即QTYPE=15。
QCLASS: 2位数据表示查询方式。
    IN       1 the Internet
CS       2 the CSNET class (Obsolete - used only for examples in some obsolete RFCs)
    CH       3 the CHAOS class
 HS       4 Hesiod [Dyer 87]
这是在rfc文档中列出的各类class,我们在这里用IN,即QCLASS=15。
 
下面使用JAVA实现的原码:
 
说明:DnsTool.IntToBytes(int,int)是将一个整数转换为几个8位数的组合。
   DnsTool.StringToBytes(String)是将一个字符串转换为QNAME需要的格式,并以BYTE[]的格式返回。
 
class DnsHeader {
 
  private int ID;
  private int Flags=0;
  private byte[] head=new byte[]{0,0,0,0,0,0,0,0,0,0,0,0};
 
  /** Creates new DnsHeader */
  public DnsHeader() 
  {
    setID();
    setFlags(Flags);
    setAnswer(false);//does not an answer
    setRecursionDesired(true);
  }
 
  private void setID()
  {
    byte[] tmp=new byte[2];
    ID=new Random().nextInt(10);
    tmp=DnsTool.IntToBytes(ID,2);
    head[0]=tmp[0];
    head[1]=tmp[1];    
  }
 
  public int getID()
  {
    return this.ID;
  }
 
  private void setFlags(int Flags) 
  {
    byte[] tmp=new byte[2];
    tmp=DnsTool.IntToBytes(ID,2);
    head[2]=tmp[0];
    head[3]=tmp[1];
 
  }
 
  public void setAnswer(boolean isAnswer)
  {
    head[2]=isAnswer?(byte)(head[2]|0x80):(byte)
(head[2]&0x7f);
  }
 
  public void setRecursionDesired(boolean isRecursionDesired) 
  {
    head[2]=isRecursionDesired?((byte)(head[2]|0x1))
:((byte)(head[2] & 0xFE));
  }
 
  public void setQDcount(int num)//set the number of question
  {
    byte[] tmp=new byte[2];
    tmp=DnsTool.IntToBytes(num,2);
    head[4]=tmp[0];
    head[5]=tmp[1];
 
  }
 
  public byte[] getBytes()
  {
    return head;
  }
}
 
class Question {
 
  private byte[] question;
  private int QuestionLength;
  /** Creates new Question */
  public Question(String questionLabel,int questionType,
int questionClass) 
  {
    byte[] transName=DnsTool.StringToBytes(questionLabel);
    byte[] ty=DnsTool.IntToBytes(questionType,2);
    byte[] cl=DnsTool.IntToBytes(questionClass,2);
 
    QuestionLength=0;
    //transfer the QuestionLabel to the bytes
    question=new byte[transName.length+4];
    System.arraycopy(transName,0,question,QuestionLength,
transName.length);
    QuestionLength+=transName.length;
 
    //transfer the type to the bytes
    System.arraycopy(ty,0,question,QuestionLength,
ty.length);
    QuestionLength+=ty.length;
 
    //transfer the class to the bytes
    System.arraycopy(cl,0,question,QuestionLength,
cl.length);
    QuestionLength+=cl.length;
  }
 
  public byte[] getBytes() 
  {
    return question;
  } 
}
这里实现了dns 的包头和要查询的question的数据,然后只要将它们组合在一起就成了dns包了,接下来就只要将它发出去就可以了,下面这段程序就实现了这一功能。
说 明: DNSSERVER:就是dns服务器的地址。 DNSPORT:dns服务器的端口,即53。 DnsQuery:这个是header 和 question 组合的数据。 DatagramPacket ID_Packet;    DatagramSocket ID_Socket;    byte[] query=DnsQuery.getBytes();    int i;        try    {      ID_Packet=new DatagramPacket(query,query.length,InetAddress.getByName(DNSSERVER),Constant.DNSPORT);      ID_Socket=new DatagramSocket();            //send query      ID_Socket.send(ID_Packet);            //close socket      ID_Socket.close();          }    catch(IOException e)    {      System.out.println(e);      return null;    }      }
下面这段程序是从Dns服务器上得到dns的返回包:
ID_Packet=new DatagramPacket(new byte[Constant.DNSUDPLEN],
Constant.DNSUDPLEN);
ID_Socket.receive(ID_Packet);
这里的变量已在上篇中定义了,Constant.DNSUDPLEN为512。
接下来就只要将这数据解压缩就可以了。这里就涉及了RR的格式了(Resource Record Format)。
   0 1 2 3 4 5 6 7 8 9 A B C D E F
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |                        |
  /                        /
  /           NAME           /
  |                        |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |           TYPE           |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |           CLASS           |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |           TTL           |
  |                        |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  |          RDLENGTH          |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
  /           RDATA           /
  /                        /
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
这是在rfc文档中定义的RR格式。
NAME:就是在question中的QNAME;
TYPE:question中的QTYPE;
CLASS:question中的QCLASS;
RDLENGTH:RDATA的长度;
RDATA:返回的数据,这才是真正有用的数据,也是我们要解析的东西。
 
因为其数据是被压缩的,所以得想知道他的压缩格式:
0 1 2 3 4 5 6 7 8 9 A B C D E F
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  | 1 1|        OFFSET          |
  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
他的压缩方式是将在数据中重复出现的字符放在一起,然后再字符出现的地方加上一个偏移位置,即如上图所示,16位的数据以11开头,后跟偏移量。偏移量是从信息的头部开始算得。下面是一个rfc文档中的例子:
0 1  2  3  4 5 6 7  8 9 A  B C D E F
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  20 |      1      |      F      |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  22 |      3      |      I      |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  24 |      S      |      I      |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  26 |      4      |      A      |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  28 |      R      |      P      |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  30 |      A      |      0      |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  40 |      3      |      F      |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  42 |      O      |      O      |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  44 | 1 1|        20            |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 
这个结果是:在40位置的域名是FOO.F.ISI.ARPA。
了解了他的压缩方式,解析就简单了。
上篇中在Header中我们已提到ANCOUNT这个字段,他表示的是回复中结果的数目,我们相见他解析出来:
public int getAnswerCount()
  {
    int INDEX=6;
    byte[] AnCountArray=new byte[2];
 
    System.arraycopy(message,INDEX,AnCountArray,0,2);
    return DnsTool.BytesToInt(AnCountArray);//将byte[]变为int
  }
得到了ANCOUNT,就可以解释结果了:
public Vector parseAnswer()
  {
    int theOffset=8;
    int pos=thePosOfAnswer;(thePosOfAnswer是你发得dns包的长度)
    int i=0,p;
    int RDlength;
    byte[] tmp;
    String Name="";
Vector IV_ Answer=new Vector();
 
    //get return name from message
    while(i<getAnswerCount())
    {
      Name="";
      //get type
      pos+=2;
      tmp=new byte[2];
      System.arraycopy(message,pos,tmp,0,2);
 
      if(DnsTool.BytesToInt(tmp)==Constant.TYPE_MX)//check the type 
      {
        pos+=theOffset;
        //get RDlength
        tmp=new byte[2];
        System.arraycopy(message,pos,tmp,0,2);
        RDlength=DnsTool.BytesToInt(tmp);
 
        pos+=4;
        p=pos;
        while((pos-p)<RDlength-2)
        {
          if((message[pos]&0xC0)==0xC0)
          {
            //this is a offset
            Name+=getPrior((message[pos]&0x3F)
|(message[pos+1]&0xFF));
            pos+=2;
          }
          else
          {
            //not offset
            tmp=new byte[message[pos]];
            System.arraycopy(message,pos+1,tmp,0,tmp.length);
            pos+=message[pos]+1;
 
            if(message[pos]!=0)
              Name+=new String(tmp)+".";
            else
              Name+=new String(tmp);
          }
        }
      }
     IV_Answer.addElement(Name);  
     i++;  
    }
  }
函数Stirng getPrior(int)是根据其偏移量等到所要的字符串,这是一个递归函数:
private String getPrior(int j)
  {
    byte[] tmp;
    String Name="";
 
    while(message[j]!=0)
    {
      if((message[j]&0xC0)==0xC0)
      {
        String mid=getPrior((message[j]&0x3F)|(message[j+1]&0xFF));
        Name+=mid;
        j+=mid.length()+1;
      }
      else
      {
        tmp=new byte[message[j]];
        System.arraycopy(message,j+1,tmp,0,tmp.length);
        j+=message[j]+1;
        if(message[j]!=0)
          Name+=new String(tmp)+".";
        else
          Name+=new String(tmp);
      }
    }
    return Name;
  }

我们只介绍了mail地址的dns解析,其他几类都大同小异,如需要可参考rfc1035。