您的位置:首页 > 大数据 > 物联网

NB-IoT使用笔记(2)实现UDP访问DNS服务获取IP地址(2)

2017-10-24 21:37 429 查看

背景

书接上回,第一版采用了python方法实现了UDP方式请求DNS服务,然而在使用单片机时是用C语言实现的,并没有python语言那么高的灵活性,考虑到此,今天使用C语言重新实现这个功能。本次实验是在Ubuntu64位机运行的,如果是windows的童鞋可以改一下对应的包含库文件。

预备知识

在编程过程中,对于DNS服务中数据包的格式有了一个更加深刻的理解。

从网上的一些博客以及相关资料中(主要参考深入理解DNS报文格式 - 夜苍山 - CSDN博客的部分内容),查到了其种一些需要注意的点,一些重要参数我会用粗体标识:

公共头报文

在请求和应答报文中有12个字节的固定长度,称为公共头报文,其内容如下



- 标识:由请求端设置的16位ID。唯一确定一组发送数据包和接收数据包,可以避免多次发送不同数据包之间混淆,可以由rand()%256 分两次得到。

- 标志:同样由请求端按位设置的报文特性,不同的位代表不同功能:



- QR 1个比特位用来区分是请求(0)还是应答(1)

- **OPCODE**E4个比特位用来设置查询的种类,应答的时候会带相同值,可用的值如下:

- 0 标准查询 (QUERY);

- 1 反向查询 (IQUERY);

- 3-15 保留值,暂时未使用;

- AA 授权应答(Authoritative Answer) - 这个比特位在应答的时候才有意义,指出给出应答的服务器是查询域名的授权解析服务器(注意因为别名的存在,应答可能存在多个主域名,这个AA位对应请求名,或者应答中的第一个主域名)。

- RA 支持递归(Recursion Available) - 这个比特位在应答中设置或取消,用来代表服务器是否支持递归查询。

- Z 保留值,暂时未使用。在所有的请求和应答报文中必须置为0。

- RCODE 应答码(Response code) - 这4个比特位在应答报文中设置,代表的含义如下:

- 0 没有错误;

- 1 报文格式错误(Format error) - 服务器不能理解请求的报文;

- 2 服务器失败(Server failure) - 因为服务器的原因导致没办法处理这个请求;

- 3 名字错误(Name Error) - 只有对授权域名解析服务器有意义,指出解析的域名不存在;

- 4 没有实现(Not Implemented) - 域名服务器不支持查询类型;

- 5 拒绝(Refused) - 服务器由于设置的策略拒绝给出应答。比如,服务器不希望对某些请求者给出应答,或者服务器不希望进行某些操作(比如区域传送zone transfer);

- 6-15 保留值,暂时未使用;

- 问题数:无符号16位整数表示报文请求段中的问题记录数,这里取1只请求一个主机的IP。

- 资源记录数:无符号16位整数,应答报文中该项内容与请求报文(一般为0)不同,在本次实验中可以通过这一项判断IP地址个数。

- 授权资源记录数:无符号16位整数。

- 额外资源记录数:无符号16位整数。

请求报文

先上图



查询码名:顾名思义,就是主机名称的ASCII了,这里需要注意的是,结尾除了用0以外,还要将主机名中的字符串根据 ‘.’ 切片,切片后的子字符串每个前面加上该字符串长度,举一个



baidu.com 就是 5 98 97 105 100 117 3 99 111 109 0 ,其种的5和3表示长度。

查询类型:两个字节表示查询类型,取值可以为任何可用的类型值,以及通配码来表示所有的资源记录。

名字数值描述
A1期望获得查询名的IP地址
NS2一个授权的域名服务器
CNAME5规范名称
PTR12指针记录
HINFO13主机信息
MX15邮件交换记录
AXFR252对区域转换的请求
ANY255对所有记录的请求
还有一些查询类型,可以在上面的博客中看到英文描述,这里没用到就不提了。

查询类:通常为1,表明是Internet数据。

这里给出一个完整的发送报文,总共长度28,请求的是 baidu.com

194 186 1 0 0 1 0 0 0 0 0 0 5 98 97 105 100 117 3 99 111 109 0 0 1 0 1

应答报文

老规矩,先上结构图,图穷匕首见嘛~



前面的三项中除了域名部分都相同,与请求报文相同。有一点不同就是,当报文中主机名重复出现的时候,该字段使用2个字节的偏移指针来表示。比如,在资源记录中,域名通常是查询问题部分的域名的重复,因此用2字节的指针来表示,具体格式是最前面的两个高位是 11,用于识别指针。其余的14位从DNS报文的开始处计数(从0开始),指出该报文中的相应字节数。一个典型的例子,C00C(1100000000001100,12正好是头部的长度,其正好指向查询名字字段)。

生存时间(TTL):以秒为单位,表示的是资源记录的生命周期,一般用于当地址解析程序取出资源记录后决定保存及使用缓存数据的时间,它同时也可以表明该资源记录的稳定程度,极为稳定的信息会被分配一个很大的值(比如86400,这是一天的秒数)。

资源数据长度:在DNS请求IP地址时,这里都是4,后面紧跟IP地址。

资源数据:该字段是一个可变长字段,表示按照查询段的要求返回的相关资源记录的数据。可以是Address(表明查询报文想要的回应是一个IP地址)或者CNAME(表明查询报文想要的回应是一个规范主机名)等。

同样,给出一个栗子(对应上面的请求报文):

194 186 129 128 0 1 0 3 0 0 0 0 5 98 97 105 100 117 3 99 111 109 0 0 1 0 1 192 12 0 1 0 1 0 0 1 206 0 4 111 13 101 208 192 12 0 1 0 1 0 0 1 206 0 4 123 125 114 144 192 12 0 1 0 1 0 0 1 206 0 4 220 181 57 217

这里的加粗的部分,第一个3表示的是baidu.com对应了三个IP地址,后面三个加粗的分别是他们的值;斜体部分可以和发送报文进行对比。

预备知识就到这里,关于上面参数的具体细节可以参考网上一些专业的资料或者专业书籍等,这里只用到了实现一个主机名请求DNS服务涉及到的内容。

代码

简单粗暴先上代码:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include<time.h>

#define MYPORT 53
#define SHOW_DATA
char* SERVERIP = "114.114.114.114";

#define ERR_EXIT(m)\
do \
{\
perror(m); \
exit(EXIT_FAILURE); \
} while(0)

void ask_ip(int sock,char * name,char show)
{
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(MYPORT);
servaddr.sin_addr.s_addr = inet_addr(SERVERIP);

int ret;
int i,j,k;
char * s = name;
unsigned char sendbuf[1024] = {0};
unsigned char host[50] = {0};
unsigned char nd1[10] = {1,0,0,1,0,0,0,0,0,0};  //数据标志,期望递归RD置位,一个问题
unsigned char nd2[5] = {0,0,1,0,1};                  //主机名结尾0,查询类型1,查询类1
unsigned char recvbuf[1024] = {0};

//产生数据包标识
sendbuf[0] = rand() % 256;
sendbuf[1] = rand() % 256;
//主机名处理
i = k = 0;
j = 1;
while(*name != 0)
{
if(*name == '.')
{
host[i] = k;
k = 0;
i = j;
j++;
name++;
}else
{
host[j] = *name;
j++;
k++;
name++;
}
}
host[i] = k;
k = j ;
j = 0;
i = 2;
for(j=0;j<10;i++,j++)
{
sendbuf[i] = nd1[j];
}
for(j=0;j<k;i++,j++)
{
sendbuf[i] = host[j];
}
for(j=0;j<5;i++,j++)
{
sendbuf[i] = nd2[j];
}
if(show)
{
printf("发送数据包:\n");
for(j=0;j<i;j++)
{
printf("%4d",sendbuf[j]);
}
printf("\n长度:%d\n",i);
}
sendto(sock, sendbuf, i, 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
if (ret == -1)
{
if (errno == EINTR)
{
printf("失败\n");
close(sock);
return;
}
ERR_EXIT("recvfrom");
}
//recvbuf[7]保存资源记录数,即IP个数
j = recvbuf[7];
k = j * 16 + i;
printf("主机  %s  对应的%2d个IP地址如下:\n",s,recvbuf[7]);
i += 12;
while(j--)
{
printf("%3d.%3d.%3d.%3d\n",recvbuf[i],recvbuf[i+1],recvbuf[i+2],recvbuf[i+3]);
i += 16;
}
if(show)
{
printf("接收数据包:\n");
for(j=0;j<k;j++)
{
printf("%4d",recvbuf[j]);
}
printf("\n长度:%d\n",k);
}
close(sock);
}

int main(int argc,char* argv[])
{
int sock;
char *inputbuf;
char show;
//随机函数种子
srand((int)time(0));
if(argc != 2 && argc != 3)
{
printf("请使用 \"./udp_dns.cpp -s (hostname)\" 或者 \"./udp_dns.cpp -u (hostname)\" 方式运行,主机名可写可不写\n");
return 0;
}else{
if(strcmp("-s",argv[1]) == 0){
show = 1;
}else if(strcmp("-u",argv[1]) == 0){
show = 0;
}else
{
printf("请使用 \"./udp_dns.cpp -s \" 或者 \"./udp_dns.cpp -u \" 方式运行\n");
return 0;
}
if(argc == 3){
inputbuf = argv[2];
}else{
inputbuf = (char *)malloc(40 * sizeof(char));
printf("请输入要解析的主机名(eg:baidu.com):\n");
scanf("%s",inputbuf);
}
}

if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0){
ERR_EXIT("socket");
}
ask_ip(sock,inputbuf,show);
return 0;
}


程序实现了基本的DNS请求获得IP地址功能,同时加入-s(显示数据) -d(屏蔽数据)选项,可以方便地查看数据包。第三个主机名参数可有可无,没有的话会在程序运行时从键盘读入。

再最后,终于可以图穷匕首见了 xx—>



最后,欢迎大家在留言区评论,交流~

转载请注明出处

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  udp c语言 ubuntu dns