标签: dns

  • prometheus bind_exporter

    prometheus bind_exporter

    named.conf

    [root@aikaiyuan ~]# cat /etc/named.conf | more
    
    statistics-channels {
        inet 127.0.0.1 port 58053 allow { 127.0.0.1; };
    };
    

    bind_exporter

    https://github.com/prometheus-community/bind_exporter/releases

    [root@aikaiyuan ~]# wget https://github.com/prometheus-community/bind_exporter/releases/download/v0.3.0/bind_exporter-0.3.0.linux-amd64.tar.gz
    [root@aikaiyuan ~]# tar zxvf bind_exporter-0.3.0.linux-amd64.tar.gz
    [root@aikaiyuan ~]# cd bind_exporter-0.3.0.linux-amd64/
    [root@aikaiyuan bind_exporter-0.3.0.linux-amd64]#  ./bind_exporter -bind.stats-url="http://127.0.0.1:58053/" -web.listen-address="192.168.1.3:59100"
    
    • -bind.stats-url=”http://127.0.0.1:58053/” 指向 named.conf statistics-channels
    • -web.listen-address=”192.168.1.3:59100″ 监听本地端口, 由 Prometheus 调用

    Make sure BIND was built with libxml2 support. You can check with the following command: named -V | grep libxml2.

    Prometheus

    https://grafana.com/grafana/dashboards/12309

    [root@aikaiyuan ~]# cat prometheus.yml
      - job_name: 'dns'
        scrape_interval: 30s
        static_configs:
        - targets:
          - 192.168.1.3:59100
          labels:
            alias: localdns
    

    转载请注明:爱开源 » prometheus bind_exporter

  • DNS什么时候使用TCP?什么时候使用UDP?

    DNS什么时候使用TCP?什么时候使用UDP?

    DNS在进行区域传输的时候使用TCP协议,其它时候则使用UDP协议;

    DNS的规范规定了2种类型的DNS服务器,一个叫主DNS服务器,一个叫辅助DNS服务器。在一个区中主DNS服务器从自己本机的数据文件中读取该区的DNS数据信息,而辅助DNS服务器则从区的主DNS服务器中读取该区的DNS数据信息。当一个辅助DNS服务器启动时,它需要与主DNS服务器通信,并加载数据信息,这就叫做区传送(zone transfer)。

    为什么既使用TCP又使用UDP?

    首先了解一下TCP与UDP传送字节的长度限制:
    UDP报文的最大长度为512字节,而TCP则允许报文长度超过512字节。当DNS查询超过512字节时,协议的TC标志出现删除标志,这时则使用TCP发送。通常传统的UDP报文一般不会大于512字节。

    区域传送时使用TCP,主要有一下两点考虑:

    1. 辅助域名服务器会定时(一般是3小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,则会执行一次区域传送,进行数据同步。区域传送将使用TCP而不是UDP,因为数据同步传送的数据量比一个请求和应答的数据量要多得多。
    2. TCP是一种可靠的连接,保证了数据的准确性。

    域名解析时使用UDP协议:

    客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。不用经过TCP三次握手,这样DNS服务器负载更低,响应更快。虽然从理论上说,客户端也可以指定向DNS服务器查询的时候使用TCP,但事实上,很多DNS服务器进行配置的时候,仅支持UDP查询包。

    如何使用 TCP

    默认使用UDP,我们也可以添加 options use-vc 使用TCP,也可以根据需求是否开启轮训 rotate https://man7.org/linux/man-pages/man5/resolv.conf.5.html

    cat /etc/resolv.conf
    options use-vc
    options rotate
    nameserver 114.114.114.114
    nameserver 114.114.115.115
    

    use-vc (since glibc 2.14)
    Sets RES_USEVC in _res.options. This option forces the
    use of TCP for DNS resolutions.
    rotate Sets RES_ROTATE in _res.options, which causes round-
    robin selection of name servers from among those
    listed. This has the effect of spreading the query
    load among all listed servers, rather than having all
    clients try the first listed server first every time.

    dig www.aikaiyuan.com +tcp
    
    queries: info: client @0xxxxxxxxxxxxx 1.1.1.1#51829 (www.aikaiyuan.com): view wangtong: query: www.aikaiyuan.com IN A + (1.1.1.1)
    queries: info: client @0xxxxxxxxxxxxx 1.1.1.1#39739 (www.aikaiyuan.com): view wangtong: query: www.aikaiyuan.com IN A +T (1.1.1.1)
    

    dns日志会添加一个 T 标志 https://kb.isc.org/docs/aa-00434

    转载请注明:爱开源 » DNS什么时候使用TCP?什么时候使用UDP?

  • Local Dns 服务器(NS记录)选择算法介绍-SRTT

    Local Dns 服务器(NS记录)选择算法介绍-SRTT

    大家都知道BIND在作为递归服务器时在向权威DNS请求时会使用优选策略,不过这个优选策略目前没有清晰的资料。小编查阅了一些公开的资料发现基本都是各种传抄,没有什么清晰的说明。因此小编专门编写此文来科普递归是如何进行优选的。本文以BIND9.8/BIND9.9/BIND9.11的代码为基础,并假定域名有多个质量不同的NS来进行计算。

    BIND9.8及之前版本的SRTT策略

    目前可以查询到的一部分公开的资料都是基于BIND9.8版本的,小编仔细查阅了BIND9.8的源代码后,判定这些公开资料的描述基本符合事实情况。小编针对BIND9.8的SRTT计算过程描述如下:

    1. 首先BIND在第一次计算SRTT时为所有的NS记录一个初始化的值,赋值方法是:
    isc_random_get(&r);
    e->srtt = (r & 0x1f) + 1;
    e->expires = 0;
    

    注释:这个值为随机1-32us,由于这个值非常小远小于正常的SRTT,因此可以认为在初始化的时候,所有的NS都会得到一个很小的近乎为零的SRTT,因此所有的NS都有机会去被第一次优选。

    1. 在所有的NS中选择SRTT最小的一个NS服务器发起解析请求,如得到应答则记录这次请求的RTT,并重新计算这个NS的SRTT,计算方法是:
    new_srtt = (addr->entry->srtt / 10 * factor)+ (rtt / 10 * (10 - factor));
    

    注释:这里的factor定义如下:

    #define DNS_ADB_RTTADJDEFAULT           7       /*%< default scale */
    #define DNS_ADB_RTTADJREPLACE           0       /*%< replace with our rtt */
    #define DNS_ADB_RTTADJAGE               10      /*%< age this rtt */
    

    因此,在正常收到应答的情况:

            factor = DNS_ADB_RTTADJDEFAULT;
    

    所以在正常的请求中,factor的值为7,所以这个新的NS的SRTT计算方法如下,也就是说这次请求的RTT在新的SRTT值的计算中权重占30%:old_srtt 0.7 + curr_rtt 0.3

    1. 在这次请求中计算了请求的NS的同时,还需要对其他的NS进行衰减计算,计算方法如下:
    if (factor == DNS_ADB_RTTADJAGE)
         new_srtt = addr->entry->srtt * 98 / 100;
    

    注释:即所有的SRTT赋值为原来的98%

    1. 如果本次NS请求以失败告终,即发出请求并没有得到应答的情况,这里就要对这个NS进行惩罚,计算方法如下:
    INSIST(no_response);
         rtt = query->addrinfo->srtt + 200000;
         if (rtt > 10000000)
         rtt = 10000000;
    

    注释:直接给SRTT加上200ms,且SRTT最大值不能超过10s

    1. 1800s后,所有的SRTT清零,重复以上的计算

    这个1800来自源码的宏定义:

    #define ADB_ENTRY_WINDOW        1800    /*%< seconds */
    

    BIND9.9及以后版本的SRTT策略

    1. 首先BIND在第一次计算SRTT时为所有的NS记录一个初始化的值,用样的赋值方法,随机1-32us。
    2. 在所有的NS中选择SRTT最小的一个NS服务器发起解析请求,如得到应答则记录这次请求的RTT,并重新计算这个NS的SRTT,同样的计算方法old_srtt 0.7 + curr_rtt 0.3
    3. 其他NS的计算方法如下:
    if (addr->entry->lastage != now) {
           new_srtt = addr->entry->srtt;
           new_srtt <<= 9;
           new_srtt -= addr->entry->srtt;
           new_srtt >>= 9;
           addr->entry->lastage = now;
    

    注释:大概值为 “SRTT = ((SRTT<<9)-SRTT)>>9″,即赋值为原来的SRTT的511/512,大概99.8%,这是BIND9.9和之前版本在计算SRTT中的一个最重要的差别

    1. 如果本次NS请求以失败告终,则惩罚方式如下:
    INSIST(no_response);
    rtt = query->addrinfo->srtt + 200000;
    if (rtt > MAX_SINGLE_QUERY_TIMEOUT_US)
           rtt = MAX_SINGLE_QUERY_TIMEOUT_US;
    

    注释:这里MAX_SINGLE_QUERY_TIMEOUT_US为宏定义,定义为

    #define MAX_SINGLE_QUERY_TIMEOUT 9U
    #define MAX_SINGLE_QUERY_TIMEOUT_US (MAX_SINGLE_QUERY_TIMEOUT*US_PER_SEC)
    

    共9s,也就是SRTT的最大值降低了1s。值得说明的是,在BIND9.11中,这里的惩罚逻辑又有了变化,计算方法如下:

    INSIST(no_response);
    isc_random_get(&value);
    if (query->addrinfo->srtt > 800000)
           mask = 0x3fff;
    else if (query->addrinfo->srtt > 400000)
           mask = 0x7fff;
    else if (query->addrinfo->srtt > 200000)
           mask = 0xffff;
    else if (query->addrinfo->srtt > 100000)
           mask = 0x1ffff;
    else if (query->addrinfo->srtt > 50000)
           mask = 0x3ffff;
    else if (query->addrinfo->srtt > 25000)
           mask = 0x7ffff;
    else
           mask = 0xfffff;
    ……
    rtt = query->addrinfo->srtt + (value & mask);
    

    注释:这里面根据当前SRTT值的不同,重新定义了一个随机数,而且是如果当前值的SRTT越小则惩罚的度量越大。

    1. 同样的1800s后,所有的SRTT清零,重复以上的计算SRTT策略&DNS解析质量。所以BIND的SRTT整个过程如下:

    SRTT从设计上来说即兼顾了DNS异常依赖的优选以及容灾措施,在所有NS的存活的情况下能够保持绝大部分的递归请求可以优选最好的NS,同时在个别NS挂掉的情况下又能容灾切换至其他的NS。同时,根据BIND版本演进中的衰减/惩罚机制变化来看, BIND在保障容灾的前提下尽可能更加选择优选(衰减策略从原来BIND9.8版本的98%变更至BIND9.9版本的99.8%),因此对于被优选NS的质量也提出了更高要求。在此小编假设一种场景,对于BIND9.11版本的递归来讲如果一直优选的那个NS因为异常原因发生了丢包从而被递归惩罚,将使用更长的时间和次数来为这个NS进行衰减,从而有更长的时间/更多的递归次数不能被优选(比如一个原本20ms的NS因为一次丢包导致SRTT增加至220ms,那么需要2300次的衰减/或者等1800s过期才能使SRTT重新恢复至20ms),这对于递归的性能有本质上的影响。

    因此,在衡量权威服务器本身性能的同时,是否拥有高质量的网络/是否拥有低丢包率的权威软硬件服务,也是重要的考量指标。在这里小编需要指出,阿里云在DNS这种互联网基础协议上持续进行基础设施的投入,使得云解析拥有全球高质量的BGP网络和自研的高性能DNS,几乎将云解析权威的丢包率降低为零,从而实现了更高质量的递归解析性能。

    相关文章:

    转载请注明:爱开源 » Local Dns 服务器(NS记录)选择算法介绍-SRTT

  • 基于 OpenResty 权威DNS 服务

    基于 OpenResty 权威DNS 服务

    NgDNS: https://github.com/selboo/ngdns-server

    FROM: https://mp.weixin.qq.com/s/oZ2ftMgPtaDCygrgOUD3QA

    最新 Nginx 已经支持 4层服务 ngx_stream_core_module, 有了 4层 支持我们可以做更多服务,

    本篇我们用 OpenResty 配合 lua-resty-dns-server 做一个高性能 权威DNS 服务

    先来看下压力测试, QPS 10W/s 左右

    # cat /tmp/q.txt
    lb.sinacloud.com
    
    # queryperf -s 127.0.0.1 -d /tmp/q.txt -l 30
    DNS Query Performance Testing Tool
    Version: $Id: queryperf.c,v 1.12 2007/09/05 07:36:04 marka Exp $
    
    [Status] Processing input data
    [Status] Sending queries (beginning with 10.23.2.241)
    [Status] Testing complete
    
    Statistics:
    
      Parse input file:     multiple times
      Run time limit:       30 seconds
      Ran through file:     3212709 times
    
      Queries sent:         3212710 queries
      Queries completed:    3212710 queries
      Queries lost:         0 queries
      Queries delayed(?):   0 queries
    
      RTT max:          0.000836 sec
      RTT min:              0.000069 sec
      RTT average:          0.000153 sec
      RTT std deviation:    0.000030 sec
      RTT out of range:     0 queries
    
      Percentage completed: 100.00%
      Percentage lost:        0.00%
    
      Started at:           Wed Jun 19 17:05:33 2019
      Finished at:          Wed Jun 19 17:06:03 2019
      Ran for:              30.000171 seconds
    
      Queries per second:   107089.722922 qps
    

    OpenResty 安装

    参考: http://openresty.org/en/installation.html

    lua-resty-dns-server 安装

    opm install vislee/lua-resty-dns-server
    

    常见DNS类型, 都以支持

    nginx.conf 配置

    stream {
    
        server {
            listen 53 udp ;
            content_by_lua_file conf/53.lua;
        }
    
    }
    

    53.lua 代码

    local server = require 'resty.dns.server'
    local sock, err = ngx.req.socket()
    if not sock then
        ngx.log(ngx.ERR, "failed to get the request socket: ", err)
        return ngx.exit(ngx.ERROR)
    end
    
    local req, err = sock:receive()
    if not req then
        ngx.log(ngx.ERR, "failed to receive: ", err)
        return ngx.exit(ngx.ERROR)
    end
    
    local dns = server:new()
    local request, err = dns:decode_request(req)
    if not request then
        ngx.log(ngx.ERR, "failed to decode request: ", err)
    
        local resp = dns:encode_response()
        local ok, err = sock:send(resp)
        if not ok then
            ngx.log(ngx.ERR, "failed to send: ", err)
            ngx.exit(ngx.ERROR)
        end
    
        return
    end
    
    local query = request.questions[1]
    ngx.log(ngx.DEBUG, "qname: ", query.qname, " qtype: ", query.qtype)
    
    local cname = "sinacloud.com"
    
    if query.qtype == server.TYPE_A then
    
        local err = dns:create_a_answer(query.qname, 600, "192.168.1.1")
        local err = dns:create_a_answer(query.qname, 600, "192.168.2.2")
        if err then
            ngx.log(ngx.ERR, "failed to create cname answer: ", err)
            return
        end
    
    end
    
    local resp = dns:encode_response()
    local ok, err = sock:send(resp)
    if not ok then
        ngx.log(ngx.ERR, "failed to send: ", err)
        return
    end
    

    dig 测试

     # dig @127.0.0.1 lb.sinacloud.com
    
    ; <<>> DiG 9.9.4-RedHat-9.9.4-74.el7_6.1 <<>> @127.0.0.1 lb.sinacloud.com
    ; (1 server found)
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45924
    ;; flags: qr rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
    ;; WARNING: recursion requested but not available
    
    ;; QUESTION SECTION:
    ;lb.sinacloud.com.      IN  A
    
    ;; ANSWER SECTION:
    lb.sinacloud.com.   600 IN  A   192.168.1.1
    lb.sinacloud.com.   600 IN  A   192.168.2.2
    
    ;; Query time: 0 msec
    ;; SERVER: 127.0.0.1#(127.0.0.1)
    ;; WHEN: Wed Jun 19 17:27:17 CST 2019
    ;; MSG SIZE  rcvd: 98
    

    后续

    目前只是实现了一个很简单的例子, 如果做成一个可持续的服务, 还需要很多工作, 比如:

    • DNS记录可以放到, Redis, MySQL 或这本地文件
    • 封装 API接口, 可以动态修改 DNS记录
    • 从 ngx.var.remote_addr 获取客户端IP, 支持智能解析
    • tld 顶级域 QPS限制
    • 记录请求日志
    • 支持递归查询, 做 LocalDNS 服务
    • 等等…

    NgDNS: https://github.com/selboo/ngdns-server

    转载请注明:爱开源 » 基于 OpenResty 权威DNS 服务

  • iptables 封禁 dns请求

    Iptables使用string match的--string选项是无法直接匹配dns查询中的域名进行操作的。

    我们先抓包看下

     # tcpdump -i lo udp port 53 -vv -nn -X
    tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes
    16:04:06.176000 IP (tos 0x0, ttl 64, id 41951, offset 0, flags [none], proto UDP (17), length 66)
        127.0.0.1.56885 > 127.0.0.1.53: [bad udp cksum 546f!] 51618+ A? testdns.topjishu.com. (38)
      0x0000:  4500 0042 a3df 0000 4011 d8c9 7f00 0001  E..B....@.......
      0x0010:  7f00 0001 de35 0035 002e fe41 c9a2 0100  .....5.5...A....
      0x0020:  0001 0000 0000 0000 0774 6573 7464 6e73  .........testdns
      0x0030:  0874 6f70 6a69 7368 7503 636f 6d00 0001  .topjishu.com...
      0x0040:  0001                                     ..
    

    可以看到, 域名 testdns.topjishu.com 的dns查询包,并不是常规的对这个域名做hex处理,其中并不包含点字符(dot character).

    dns包中,不包含点字符,而是对点字符进行分割,每部分的开始处加入这一段的字符长度。

    • testdns 长度 7
    • topjishu 长度 8
    • com 长度 3

    使用 iptables –hex-string 来封禁域名

    --hex-string "|07|testdns|08|topjishu|03|com"
    

    iptables封禁命令

    iptables -I INPUT -p udp --dport 53 -m string --hex-string "|07|testdns|08|topjishu|03|com" --algo bm --to 1480 -j DROP
    iptables -I INPUT -p tcp --dport 53 -m string --hex-string "|07|testdns|08|topjishu|03|com" --algo bm --to 1480 -j DROP
    

    另外,这里--algo指定匹配字符串的算法

    `–algo {bm|kmp}

         Select the pattern matching strategy. (bm = Boyer-Moore, kmp = Knuth-Pratt-Morris)`
    

    有两个选择,bm算法和kmp算法,kmp算是一个众所周知的字符串匹配算法了,而bm是另一个更高效、精妙的算法。

    详细可以看看:

    参考:

    转载请注明:爱开源 » iptables 封禁 dns请求

  • couldn’t add command channel 127.0.0.1#953: address in use

    Spent sometime around this error using chroot bind 9.x
    couldn’t add command channel 127.0.0.1#953: address in use

    Guys, assuming your bind config is correct – this error is related to service “portserve” running and using this port!

    ps -ef|grep portreserve|grep -v grep
    root      1349     1  0 May06 ?        00:00:00 /sbin/portreserve
    

    stop the service and disable from startup:

    service portreserve stop
    Stopping portreserve: [  OK  ]
    chkconfig portreserve off
    

    转载请注明:爱开源 » couldn’t add command channel 127.0.0.1#953: address in use

  • 编译bind9支持edns-client-subnet

    编译bind9支持edns-client-subnet

    智能DNS智能否

    众所周知,DNS解析是我们访问internet的“第一跳”,若域名解析失常那是一件很可怕的事情。所以这一步走的需要更快,更稳。现在业界普遍采用了智能DNS来实现,其原理就是根据自身所保存的表项的ip和用户的所在位置对应来就近分配服务器的主机记录给用户。智能DNS现在用途很广泛,如:在CDN网络中,根据智能DNS实现GSLB全局负载均衡,就近分发可请求的缓存服务器(也有可能是下级GSLB的site ip);在跨多数据中心组网,其也很好的实现异地多活。等等。但是智能DNS真的是完美,智能吗?在DNS解析中,帮我们出去向根递归的是LDNS,如果我们的LDNS和本地用户不在一个地理位置,那么用户则会得到一个LDNS所在位置最近的IP地址,例如我们很多人喜欢吧LDNS设置为8.8.8.8(google的公开DNS,稳定),这样我们智能DNS则会让用户得到一个美国服务器IP这显然是不合理的。

    做个小实验

    先将LDNS指定google的8.8.8.8,再将LDNS指定为学校的DNSserver

    $ dig -t A www.alibaba.com @8.8.8.8
    
    ; <<>> DiG 9.10.3-P4-Ubuntu <<>> -t A www.alibaba.com @8.8.8.8
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3653
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
    
    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags:; udp: 512
    ;; QUESTION SECTION:
    ;www.alibaba.com.   IN  A
    
    ;; ANSWER SECTION:
    www.alibaba.com.    164 IN  CNAME   www.gds.alibaba.com.
    www.gds.alibaba.com.    114 IN  A   198.11.132.23
    
    ;; Query time: 201 msec
    ;; SERVER: 8.8.8.8#53(8.8.8.8)
    ;; WHEN: Sun Jul 24 22:04:38 CST 2016
    ;; MSG SIZE rcvd: 82
    

    耗时201ms,解析结果为198.11.132.23,所在地美国,GeoIP: San Mateo, California, United States

    $ dig -t A www.alibaba.com @202.119.160.11
    ; <<>> DiG 9.10.3-P4-Ubuntu <<>> -t A www.alibaba.com @202.119.160.11
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18762
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 2, ADDITIONAL: 1
    
    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags:; udp: 4096
    ;; QUESTION SECTION:
    ;www.alibaba.com. IN A
    
    ;; ANSWER SECTION:zhiwei
    www.alibaba.com. 300 IN CNAME www-cn.gds.alibaba.com.
    www-cn.gds.alibaba.com. 120 IN A 106.11.62.61
    
    ;; AUTHORITY SECTION:
    gds.alibaba.com. 6460 IN NS gdsns1.alibaba.com.
    gds.alibaba.com. 6460 IN NS gdsns2.alibaba.com.
    
    ;; Query time: 34 msec
    ;; SERVER: 202.119.160.11#53(202.119.160.11)
    ;; WHEN: Sun Jul 24 22:08:59 CST 2016
    ;; MSG SIZE rcvd: 127
    

    耗时34ms,解析结果为106.11.62.61,所在地上海,GeoIP: Hangzhou, Zhejiang, China

    solution

    面对这个情况,现在我知道的有以下两种解决方案:

    • HTTPDNS 来源于腾讯,将DNS请求借由HTTP来向HttpDNS接口发起查询。于绕过了运营商的LocalDNS,用户解析域名的请求通过Http协议直接透传到了腾讯的HttpDNS服务器IP上,用户在客户端的域名解析请求将不会遭受到运营商解析转发,DNS污染,劫持,出口多NAT等等困扰
    • ECS:来源于google,对EDNS0的扩展。在DNS请求的包中插入client的网段地址带到ADNS上,ADNS收到ECS段位的包根据client的IP去进行DNS记录匹配,google的公开name server已支持,此方式需要LDNS和ADNS(授权域名服务器)同时支持。现在大多数有CDN的厂商都支持了edns-client-subnet,但是运营商的LDNS都不支持,所以现在还是为正式被广泛使用。~~原生的bind目前还不支持~~目前只有在最新的bind9.11才被支持,而一般较新版本的bind-utils中的dig工具已经支持了此项的调试,无需编译就可使用。具体使用见后面的测试

    PS:就小的看来HTTPDNS会越来越火,DNSPod也推出了相应的方案,听说现在已经集成到一部分app 的SDK里面了,做为正常DNS解析失常的备用方案。有时间研究下另开一篇胡说八道

    环境搭建(ADNS测试)

    编译bind

    edns-client-subnet(下简称ecs)其现在还没有正式被bind9支持,但是bind9给出了实验版本。需要对bind重新编译,ISC上有支持ecs authoritative的源码,git克隆到到本地编译即可。

    完成的环境:

    [root@localhost ~]# uname -a
    Linux localhost.localdomain 2.6.32-431.el6.x86_64 #1 SMP Fri Nov 22 03:15:09 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux
    

    安装

    git clone https://source.isc.org/git/bind9.git
    cd bind9/
    ./configure
    make && make install
    
    设置ECS elements

    设置bind的主配置文件,编辑/etc/named.conf:

    [root@localhost ~]# vim /etc/named.conf
    acl localnet{
      ecs 120.0.0.1/24;
    };
    acl externet{
      any;
    };
    options {
      directory "/var/named";       # 指定bind主目录
    };
    view internal {     #intermal视图配置
      match-clients { localnet; };  #只有匹配localnet的用户进入internal域解析
      recursion yes;        #允许递归
      zone "." IN { #根域解析
        type hint;  #区域类型为提示域
        file "named.ca";    #指定根域的解析文件,可用dig -t a .>/var/named/named.ca生成
      };
    
      zone "zhxfei.com" IN {
        type master;
        file "zhxfei.com.localnet.zone";    #指定区域解析文件
        allow-transfer { none; };
      };
    };
    view external {
      match-clients { externet; }; #不匹配ecs的用户进入此视图解析
      recursion no; #不允许递归
      zone "." IN { #根域解析
        type hint;  #区域类型为提示域
        file "named.ca";    #指定根域的解析文件,可用dig -t NS .>/var/named/named.ca生成
    };
    
      zone "zhxfei.com" IN {
        type master;    #指定bind对这个区域的解析角色
        file "zhxfei.com.externnet.zone";   #指定区域解析文件
        allow-transfer { none; };   #不允许区域传送
      };
    };
    

    如上,在ADNS上的bind上,写acl来匹配ecs对应的ip,来看其作用:

    ACLs can now include “ecs” elements which specify
    an address or network prefix; if an ECS option is
    included in a DNS query, then the address encoded
    in the option will be matched against “ecs” ACL
    elements.
    Also, if an ECS address is included in a query,
    then it will be used instead of the client source
    address when matching “geoip” ACL elements. This
    behavior can be overridden with “geoip-use-ecs no;”.
    When “ecs” or “geoip” ACL elements are used to
    select a view for a query, the response will include
    an ECS option to indicate which client network the
    answer is valid for.

    简单翻译下:即在ADNS上如果ECS地址包含在查询内,它将使用其代替源IP进行解析查询(在没有使用“geoip-use-ecs no的情况下”),并将ECS 的网段地址写进response报文中,回复给LDNS,表明采用了ecs解析。

    查询过程是这样的:

    在我写的规则中,ACL localnet只有120.0.0.1/24这个网段,只有在收到的query报文中有ecs段位且匹配到了这个ecs的网段,才能命中internal视图

    测试准备

    通过 named 来开启bind进程,之后通过查看日志检查bind是否正常启动,以及端口是否开放正常

    named
    tail /var/log/message
    netstat -tunlp | grep named
    

    可以看到bind和rndc开放正常:

    [root@localhost ~]# netstat -tunlp | grep named
    tcp        0      0 172.16.130.129:53           0.0.0.0:*                   LISTEN      2124/named
    tcp        0      0 127.0.0.1:53                0.0.0.0:*                   LISTEN      2124/named
    tcp        0      0 127.0.0.1:953               0.0.0.0:*                   LISTEN      2124/named
    tcp        0      0 :::53                       :::*                        LISTEN      2124/named
    tcp        0      0 ::1:953                     :::*                        LISTEN      2124/named
    udp        0      0 172.16.130.129:53           0.0.0.0:*                               2124/named
    udp        0      0 127.0.0.1:53                0.0.0.0:*                               2124/named
    udp        0      0 :::53                       :::*                                    2124/named
    
    验证结果

    在本地用dig调试

    用200.0.0.1/24作为用户所在的网段:

    $ dig -t A www.zhxfei.com @172.16.130.129 +subnet=200.0.0.1/24
    
    ; <<>> DiG 9.10.3-P4-Ubuntu <<>> -t A www.zhxfei.com @172.16.130.129 +subnet=200.0.0.1/24
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11779
    ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 2
    ;; WARNING: recursion requested but not available
    
    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags:; udp: 4096
    ; CLIENT-SUBNET: 200.0.0.0/24/0
    ;; QUESTION SECTION:
    ;www.zhxfei.com. IN A
    
    ;; ANSWER SECTION:
    www.zhxfei.com. 86400 IN A 1.1.1.1
    
    ;; AUTHORITY SECTION:
    zhxfei.com. 86400 IN NS ns.zhxfei.com.
    
    ;; ADDITIONAL SECTION:
    ns.zhxfei.com. 86400 IN A 172.16.130.129
    
    ;; Query time: 0 msec
    ;; SERVER: 172.16.130.129#53(172.16.130.129)
    ;; WHEN: Mon Jul 25 01:00:43 CST 2016
    ;; MSG SIZE rcvd: 103
    

    用120.0.0.1/24作为用户所在的网段

    $ dig -t A www.zhxfei.com @172.16.130.129 +subnet=120.0.0.1/24
    
    ; <<>> DiG 9.10.3-P4-Ubuntu <<>> -t A www.zhxfei.com @172.16.130.129 +subnet=120.0.0.1/24
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 21300
    ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 2
    
    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags:; udp: 4096
    ; CLIENT-SUBNET: 120.0.0.0/24/24
    ;; QUESTION SECTION:
    ;www.zhxfei.com. IN A
    
    ;; ANSWER SECTION:
    www.zhxfei.com. 86400 IN A 10.0.0.1
    
    ;; AUTHORITY SECTION:
    zhxfei.com. 86400 IN NS ns.zhxfei.com.
    
    ;; ADDITIONAL SECTION:
    ns.zhxfei.com. 86400 IN A 172.16.130.129
    
    ;; Query time: 0 msec
    ;; SERVER: 172.16.130.129#53(172.16.130.129)
    ;; WHEN: Mon Jul 25 01:00:53 CST 2016
    ;; MSG SIZE rcvd: 103
    

    验证如上,匹配到ecs elements的查询替代了源172.16.130.1的解析结果

    抓包

    附带抓包结果,有兴趣看一下:
    第一次解析


    第二次解析

    其他

    可能出现的问题

    实验的时候可能会出现这样的回复:

    原因是DNS server的防火墙策略将query REJECT,关闭iptables即可

    f5不支持edns-client-subnet

    现在很多智能DNS都做在了专业的应用交付设备上,常用的有三款:F5的GTM(广域网流量管理器),citrix的NetScaler,还有radware linkproof。
    其中F5的BIG-IP系列的GTM独占鳌头。但是遗憾的是不支持edns-client-subnet,即当其收到带有ecs段位的报文,会将请求drop掉。
    详细情况请看sol16240: The BIG-IP system treats as malformed and drops DNS queries containing the EDNS Client subnet option

    转载请注明:爱开源 » 编译bind9支持edns-client-subnet

  • DNS协议详解

    DNS协议详解

    DNS属于应用层的协议,DNS提供了将人类易于理解的主机名或域名转换为计算机或网络可识别的数字地址的机制,从而使得互连网的广泛应用成为可能。 一、 DNS涉及的基本概念
    (1)域名及顶级域
    1)域名
    域名(Domain Name)通常是用户所在的主机名。域名格式是由若干部分组成,每个部分又称子域名,它们之间用“.”分开,每个部分最少由两个字母或数字组成。城名通常按分层结构来构造,每个子域名都有其特定的含义。从右到左,子域名分别表示不同的国家或地区的名称(只有美国可以省略表示国家的顶级域名)、组织类型、组织名称、分组织名称和计算机名称等,如www.example.com既是一个域名的典型例子。
    2)域名地址的最后一部分子域名称为高层域名(或顶级域名)
    它大致上可以分成两类:一类是组织性顶级域名;另一类是地理性顶级域名。
    ① 组织性顶级域名的出发点是为了说明拥有并对那些Internet主机负有责任的组织的类型。 表7-36给出部分组织的分类及它们所对应的组织性顶级域名。

    表7-36 组织性顶级域名

    ????② 组织性域名除了国际性组织域名外,其它类型的组织在Internet诞生时就已存在了,但随着Internet的日益国际化,这种组织性顶级域名已难以满足需求了,于是便产生一种新的地理性顶级域名。地理性项级域名用两个字母的缩写形式来完全地表示某个国家和地区。
    (2)域名系统的构成
    Internet中的域名地址与IP地址是等价的,它们之间通过域名系统DNS进行映射变换。实际上,DNS系统是一种分布式地址信息数据库系统,采用客户机/服务器模式,服务器中包含整个数据库的某部分信息,并供客户查询。DNS允许局部控制整个数据库的某些部分,但数据库的每一部分都可通过全网查询得到。
    网络上的每台主机都有域名,指向主机相关信息,像IP地址等。主机也可以有一个或多个域的别名,它只是简单地从一个域名(别名)指向另一个域名(正式域名)。DNS采用层次结构的优点:各个组织在它们的内部可以自由地选择域名,只要保证组织内的惟一性即可,而不必担心与其它组织内的域名相冲突。
    域名系统采用的是客户机/服务器模式,由三部分构成:域名数据库、域名服务器和地址解析器。
    各自功能如下:
    • 地址解析器是客户方,负责查询域名服务器、解释从服务器返回来的应答、将信息返回给请求方等工作。
    • 域名服务器(Domain Name Server)是服务器方,存储并管理着所管辖区域的域名数据库,负责接收来自于地址解析器的请求,按照请求类型,进行递归与非递归查询。同时将查询结果返回给地址解析器(网络中的每台主机既可作为客户方,也可作为服务器方)。
    • 域名数据库
    域名数据库是一个大型的分布在整个网络上的分布式数据库,存储了按层次管理的相关的数据,该层次结构可理解为一棵倒放的树(见图10-2),树中的每个结点均代表一个域,并存储着与该域相关的区域和资源记录(RRs),以供域名服务器查询使用。域名系统的构成如图7-37所示。

    图7-37 DNS结构

    ????(3)资源记录
    在一个域名服务器的配置文件里包含若干个资源记录,以帮助地址解析器进行地址解析。配置文件中资源记录所含的信息见表7-38资源记录示例,表中有关记录的内容及意义参照有关DNS报文格式进行解读。

    表7-38配置文件中资源记录示例

    ????(4)域名数据的分类
    域名数据分为两类数据:授权数据和缓冲数据。
    1)授权数据
    授权数据是经过授权的数据,该数据来自于负责存储该类数据的域名服务器,授权数据是最近得到的新数据,因而被认为是正确数据。该类数据使用价值高,但为了获得授权数据其时间花费的代价也高。通常主机在发出地址解析请求时要在报文头中要指出是否需要授权数据(见图10-30报文头格式)。
    2)缓冲数据
    缓冲数据是早先请求或交互操作的应答数据,被主机存储在本地缓冲区内。这类数据与授权数据相比,使用价值略低一些,但时间花费的代价低。通常,在互联网稳定状态下,缓冲数据在某种程度上也是很有用的。
    (5)域名区域
    如果整个网络中只有一台域名服务器且其中存储了所有的DNS的信息,那么域名转换处理就简单的多了,因为网络中任意一个主机对域名查询的请求均发向这惟一的一台域名服务器,它负责进行查询处理并将结果反馈给发出查询的主机即可。但是这种解决方案无论从数据的存储量还是从数据的传输速度要求都无法适应不断变化的网络及用户的需求。为了满足变化的网络及庞大数据的存储量还是从数据的传输速度,DNS采用了一种树形结构对域名数据库进行组织和管理,树中的每一个节点代表域名系统的域。域可以进一步划分成子域,每个域都有一个域名并由不同的组织管理,并定义了它在数据库中的位置。我们已经知道,在DNS中,一个完整的域名是从该域向上直到根的所有标记组成的字符串,标记之间要用“.”分隔开。
    1)域名区域的定义
    为了便于对树中各节点代表域名信息进行有效的管理,就引出了域名区域的概念。域名区域是指一个域名服务器负责管理的命名空间的一部分。域名服务器负责维护自己管理区域的授权数据。域名区域构成见图10-2。
    域名区域是由资源记录中的一组授权数据定义的。其内容有:
    • 域名区域中所有节点的资源记录。
    • 域名区域中顶级节点的信息。如在图7-39中,Exam2.com区域的顶级节点是Exam2.com,Exam2.com节点可保存管理该区域所有节点的信息。
    • 委托子区域信息(被委托给本区域中其它子区域管理的区域)。


    图7-39 DNS区域和子区域示意图

    ????2)利用域名区域信息进行域名地址解析
    当某个主机请求获得域名payroll.h2.Exam2.com的IP地址时,其地址解析程序首先从树的根节点获得域.com 的域名信息,然后从.com的域名服务器获得其管辖下的子区域Exam2.com的域名信息,这样逐级向下进行查询,直到到达payroll.h2.Exam2.com子区域的域名服务器,并从中获得其相应的IP地址,并反馈给地址解析程序。
    (6)递归查询与非递归查询
    综上所述,一个单一的域名服务器无法对每一个域名查询进行完整的回答,但是它可以对查询路径作出准确的响应。即,当一个域名服务器存储了一个域名查询所请求域的所有授权信息,它可直接给出需要的查询结果,否则,它必须给出具有所需信息的最近的域名服务器,以便继续查询。所谓最近的域名服务器一般是图7-39所示的区域树中父节点的域名服务器,一旦查询到具有目标域名信息的域名服务器,该域名服务器自动将目标地址信息发送到请求方。否则该域名服务器继续向请求方推荐另一个更接近目标的域名服务器,上述过程一直持续到请求方得到正确的结果或在使用授权信息访问某个域名服务器时出现错误为止。这种查询过程可能会反复多次。
    1) 递归查询
    所谓递归查询是指接收请求的第一个域名服务器必须自始至终对请求进行处理,或向其它域名服务器进行请求且最终获得授权数据,并对请求进行应答。采用递归查询时,当所请求的域名信息在自身缓冲区时,域名服务器直接返回缓冲数据。此时递归查询请求标志无效(见DNS报头介绍)。有关递归查询见图7-40所示。
    2)非递归查询
    所谓非递归查询是指接收请求的第一个域名服务器可以返回可靠数据(本身有时),也可以返回指向其它服务器的指针(相当于将查询的接力棒传给了最接近的域名服务器)。递归查询与非递归查询的区别可从图7-40看出。
    主机的地址解析程序在查询时可以指定是否用递归还是非递归查询方式。非递归查询方式与递归查询方式相比响应速度快。
    二、DNS的报文格式
    DNS报文由报头和正文段构成,DNS有四类正文段:查询段、应答段、授权段和附加段。其具体构成见7-41所示。

    图7-41 DNS报文结构

    ???? 其中正文段中的查询段用于主机向域名服务器发出地址解析请求,应答段、授权段、附加段用于域名服务器向主机返回地址解析结果。DNS报文头和正文段的格式具体介绍如下。
    (1) DNS报文报头格式
    DNS报文报头格式如图7-42所示。

    7-42 DNS报头格式

    ????各个字段意义简述如下:
    • ID:这是由生成DNS查询的程序指定的16位的标志符。该标志符也被随后的应答报文所用,申请者利用这个标志将应答和原来的请求对应起来。
    • QR:该字段占1位,用以指明DNS报文是请求(0)还是应答(1)。
    • OPCODE:该字段占4位,用于指定查询的类型。值为0表示标准查询,值为1表示逆向查询,值为2表示查询服务器状态,值为3保留,值为4表示通知,值为5表示更新报文,值6~15的留为新增操作用。
    • AA:该字段占1位,仅当应答时才设置。值为1,即意味着正应答的域名服务器是所查询域名的管理机构或者说是被授权的域名服务器。
    • TC:该字段占1位,代表截断标志。如果报文长度比传输通道所允许的长而被分段,该位被设为1。
    • RD:该字段占1位,是可选项,表示要求递归与否。如果为1,即意味 DNS解释器要求DNS服务器使用递归查询。
    • RA:该字段占1位,代表正在应答的域名服务器可以执行递归查询,该字段与查询段无关。
    • Z:该字段占3位,保留字段,其值在查询和应答时必须为0。
    • RCODE:该字段占4位,该字段仅在DNS应答时才设置。用以指明是否发生了错误。
    允许取值范围及意义如下:
    0:无错误情况,DNS应答表现为无错误。
    1:格式错误,DNS服务器不能解释应答。
    2:严重失败,因为名字服务器上发生了一个错误,DNS服务器不能处理查询。
    3:名字错误,如果DNS应答来自于授权的域名服务器,意味着DNS请求中提到的名字不存在。
    4:没有实现。DNS服务器不支持这种DNS请求报文。
    5:拒绝,由于安全或策略上的设置问题,DNS名字服务器拒绝处理请求。
    6 ~15 :留为后用。
    • QDCOUNT:该字段占16位,指明DNS查询段中的查询问题的数量。
    • ANCOUNT:该字段占16位,指明DNS应答段中返回的资源记录的数量,在查询段中该值为0。
    • NSCOUNT:该字段占16位,指明DNS应答段中所包括的授权域名服务器的资源记录的数量,在查询段中该值为0。
    • ARCOUNT:该字段占16位,指明附加段里所含资源记录的数量,在查询段中该值为0。
    (2)DNS正文段
    在DNS报文中,其正文段封装在图7-42所示的DNS报文头内。DNS有四类正文段:查询段、应答段、授权段和附加段。
    1)查询段的格式
    图7-43给出了查询段的格式。各字段意义为:
    • QNAME:该字段是可变长字段,其中包含一个被请求的域名,用一系列标签表示,每一个标签由一个八进制后面跟着一个表示长度的八进制数组成。
    • QTYPE: 该字段占16位,指定查询的资源类型(Type),该字段将一个类型值与一条指定的资源记录相匹配(有些通用的QTYPE值可以和与多条资源记录相匹配),其值可以是A(请求主机IP地址)、NS(请求授权域名服务器)或CNAME(请求返回规范名称,或者是某主机使用的与别名对应的真实名称)。
    • QCLASS: 该字段占16位,指定查询的类别(Class),如Inet用以表示互联网和IP地址查询。

    图7-43 DNS报文中查询段的格式

    ????2)应答段、授权段、附加段的格式
    查询段是主机向域名服务器发出的将域名转换为IP地址的请求报文,域名服务器按照主机查询类型,经过查询资源记录数据库后返回含有资源记录的应答段、授权段或附加段,资源记录告诉主机所查询的信息。应答段、授权段、附加段具有相同的格式,其格式如图7-44所示。
    各字段意义介绍如下:
    • NAME:该字段是可变长字段,资源记录对应的域名(与主机发出的查询段中QNAME相同)。
    • TYPE:占16位,该字段与查询段中的QTYPE相同。
    • CLASS:占16位,该字段与查询段中的QCLASS相同。
    • TTL:占32位,该字段表示资源记录的生命周期(以秒为单位),一般用于当地址解析程序取出资源记录后决定保存及使用缓存数据的时间。
    • RDLENTH:占16位,该字段表示资源数据的长度(以字节为单位)。
    • RDATA:该字段是可变长字段,表示按查询段要求返回的相关资源记录的数据。其TYPE值是A,则返回4个字节的主机IP地址,如果TYPE值是NS,则返回授权域名服务器的域名,如果TYPE值是CNAME,则返回规范名称,或者是该主机使用的与别名对应的真实名称)。


    图7-44DNS应答段、授权段或附加段的格式

    ????三、DNS的工作过程及示例
    (1)DNS的工作过程
    域名系统是一个分布式系统,其管理和控制也是分布式的。一个用户A在查找另一用户B的域名时,域名系统的工作过程如图7-45所示。


    图7-45 DNS基本工作过程

    ???? 在DNS查找域名的过程中,域名服务器为了得到一个IP地址常常需要查询多个域名服务器。于是,在查询地址的同时,本地域名服务器也就得到了许多其它域名服务器的信息,像它们的IP地址、所负责的区域等,本地域名服务器将这些信息连同最终查询到的主机IP地址全部存放在它的缓冲区中,以便将来参考。当下次解析器再查询与这些域名相关的信息时,就可以直接引用。这样,就大大减少了查询时间。
    (2)DNS工作实例
    以下给出一个要求查找域名是www.internet-standard.com的IP地址的DNS查询实例。
    1)DNS查询报文
    查询主机发出的DNS查询报文见图7-46所示。其DNS查询报文中报头意义如下:
    • QR=0:表示为查询段。
    • OPCODE=0000:表示为标准查询。
    • AA=0:表示为未要求授权。
    • TC=0:未截断。
    • RD=1:表示为要求递归查询。
    • RA=0:该项与应答有关,与查询无关,因此设置为零。
    • Z=000:属保留位
    • RCODE=0000:该项是对应答信息的设置,与查询无关,故均设置为0。
    • QDCOUNT=1:该项表示只有1条查询信息。
    • ANCOUNT=0:该项表示应答时返回资源记录的数量,由于是查询信息,故设为0。
    • NSCOUNT=0:该项表示应答时返回授权服务器资源记录的数量,对于查询段,应设置为零。
    • ARCOUNT=0:该项表示应答时返回附加的授权域名服务器资源记录的数量,对于查询段,应设置为零。
    • QNAME=www.internet-standard.com:该项给出要求查询的域名。
    • QTYPE=A:该项表示要求查询IP地址。
    • QCLASS=inet:该项表示互联网的IP地址查询。


    图7-46 查询 DNS报文内容

    ????2)DNS应答报文
    相关域名服务器收到DNS查询报文后,进行解包分析,通过判定,确定起为一般的递归查询报文,要查询的是域名为www.internet-standard.com,并且得知要求返回对应的IP地址,经过一系列的查询处理,获得了相应的资源记录RRs,返回与上述DNS查询段对应的DNS应答报文,具体应答报文如图7-47所示。
    ① DNS应答报头解释
    在DNS应答报头中,只需修改与应答有关的字段:QR、RA、RCODE、ANCOUNT、NSCOUNT、ARCOUNT。
    • QR=1:表示为应答段。
    • OPCODE=0000:表示为标准查询。
    • AA=0:表示为未要求授权。
    • TC=0:未截断。
    • RD=1:表示为要求递归查询。
    • RA=1:表示正在应答的域名服务器可以执行递归查询。
    • Z=000:属保留位
    • RCODE=0000:该项是对应答情况的设置,其值为零表示无错误。
    • QDCOUNT=1:该项表示只有1条查询信息。
    • ANCOUNT=2:该项表示应答时返回资源记录的数量为2条。
    • NSCOUNT=2:该项表示应答时返回授权服务器资源记录的数量为2条。
    • ARCOUNT=0:该项表示应答时返回附加的授权域名服务器资源记录的数量为0。
    • QNAME=www.internet-standard.com:该项给出要求查询的域名。
    • QTYPE=A:该项表示要求查询IP地址。
    • QCLASS=inet:该项表示互联网的IP地址查询。
    ② DNS应答部分查询段解释
    在DNS应答报文中,仍需将原查询段的内容附加在报头其后。但内容不变。
    ③ DNS应答部分的第一条资源记录解释
    • NAME= www.internet-standard.com:要查询的域名,即资源记录中对应的域名。
    • TYPE=CNAME:意味着www.internet-standard.com是别名。
    • CLASS= inet:表示是互联网。
    • TTL=60:该资源记录的生命周期是60秒(以秒为单位)。
    • RDLENTH=2:表示资源数据的长度为2个字节(以字节为单位),此处是指针程度。
    • RDATA= internet-standard.com:该主机使用的与别名对应的真实名称。
    ④ DNS应答部分的第二条资源记录解释
    • NAME= internet-standard.com:要查询的主机的真实域名,由上一条资源记录返回的“RDATA”的值。
    • TYPE=A:表示要查询的是internet-standard.com 对应的IP地址。
    • CLASS= inet:表示是互联网。
    • TTL=60:同上。
    • RDLENTH=4:表示资源数据的长度为4个字节(RDATA所表示的IP地址长度)。
    • RDATA=216.92.98.204:该主机真实域名所对应的IP地址。
    ⑤ DNS应答部分的第一条授权资源记录解释
    NAME= internet-standard.com:要查询的主机的真实域名。
    • TYPE=NS:返回的资源记录是授权服务器的域名。
    • CLASS= inet:表示是互联网。
    • TTL=60:同上。
    • RDLENTH=1:表示资源数据的长度为11个字节。
    • RDATA=ns00.ns0.com:对请求域进行管理的授权服务器的域名。
    ⑥ DNS应答部分的第二条授权资源记录解释
    • NAME= internet-standard.com:要查询的主机的真实域名。
    • TYPE=NS:返回的资源记录是授权服务器的域名。
    • CLASS= inet:表示是互联网。
    • TTL=60:同上。
    • RDLENTH=13:表示资源数据的长度为13个字节。
    • RDATA= ns130.pair.com:对请求域进行管理的另一个授权服务器的域名。

    转载请注明:爱开源 » DNS协议详解

  • Wireshark分析DNS 协议

    Wireshark分析DNS 协议

    摘要:

    本文简单介绍了DNS协议理论知识,给出URL解析步骤,详细讲述了DNS报文各个字段含义,并从Wireshark俘获分组中选取DNS相关报文进行分析。

    一、概述

    1.1 DNS

    识别主机有两种方式:主机名、IP地址。前者便于记忆(如www.yahoo.com),但路由器很难处理(主机名长度不定);后者定长、有层次结构,便于路由器处理,但难以记忆。折中的办法就是建立IP地址与主机名间的映射,这就是域名系统DNS做的工作。DNS通常由其他应用层协议使用(如HTTP、SMTP、FTP),将主机名解析为IP地址,其运行在UDP之上,使用53号端口。

    注:DNS除了提供主机名到IP地址转换外,还提供如下服务:主机别名、邮件服务器别名、负载分配。

    1.2 HTTP使用DNS情形

    考虑这样的操作,在浏览器输入http://www.baidu.com/index.html并回车,首先需要将URL(存放对象的服务器主机名和对象的路径名)解析成IP地址,具体步骤为:

    (1)同一台用户主机上运行着DNS应用的客户机端(如浏览器)

    (2)从上述URL抽取主机名www.baidu.com,传给DNS应用的客户机端(浏览器)

    (3)该DNS客户机向DNS服务器发送一个包含主机名的请求(DNS查询报文)

    (4)该DNS客户机收到一份回答报文(即DNS回答报文),该报文包含该主机名对应的IP地址119.75.218.70

    (5)浏览器由该IP地址定位的HTTP服务器发送一个TCP链接

    用Wireshark捕获的DNS报文如下图,显然第一行是DNS查询报文,第二行是DNS回答报文。

    图1 Wireshark捕获的DNS报文

    二、DNS报文

    2.1 DNS报文格式

    DNS只有两种报文:查询报文、回答报文,两者有着相同格式,如下:

    图2 DNS报文格式

    2.1.1 首部区域

    标识数

    对该查询进行标识,该标识会被复制到对应的回答报文中,客户机用它来匹配发送的请求与接收到的回答。

    标志[1]

    图3 DNS报文首部区域的标志

    QR(1比特):查询/响应的标志位,1为响应,0为查询。

    opcode(4比特):定义查询或响应的类型(若为0则表示是标准的,若为1则是反向的,若为2则是服务器状态请求)。

    AA(1比特):授权回答的标志位。该位在响应报文中有效,1表示名字服务器是权限服务器(关于权限服务器以后再讨论)

    TC(1比特):截断标志位。1表示响应已超过512字节并已被截断(依稀好像记得哪里提过这个截断和UDP有关,先记着)

    RD(1比特):该位为1表示客户端希望得到递归回答(递归以后再讨论)

    RA(1比特):只能在响应报文中置为1,表示可以得到递归响应。

    zero(3比特):不说也知道都是0了,保留字段。

    rcode(4比特):返回码,表示响应的差错状态,通常为0和3,各取值含义如下:

    0 无差错

    1 格式差错

    2 问题在域名服务器上

    3 域参照问题

    4 查询类型不支持

    5 在管理上被禁止

    6 — 15 保留

    问题数、回答RR数、权威RR数、附加RR数

    这四个字段都是两字节,分别对应下面的查询问题、回答、授权和附加信息部分的数量。一般问题数都为1,DNS查询报文中,资源记录数、授权资源记录数和附加资源记录数都为0[1]。

    2.1.2 区域

    (1)问题区域

    包含正在进行的查询信息。包含查询名(被查询主机名字的名字字段)、查询类型、查询类。

    图4 DNS报文的问题区域

    查询名

    查询名部分长度不定,一般为要查询的域名(也会有IP的时候,即反向查询)。此部分由一个或者多个标示符序列组成,每个标示符以首字节数的计数值来说明该标示符长度,每个名字以0结束。计数字节数必须是0~63之间。该字段无需填充字节。还是借个例子来说明更直观些,查询名为gemini.tuc.noao.edu的话,查询名字段如下[1]:

    图5 DNS报文问题区别的查询名

    查询类型

    通常查询类型为A(由名字获得IP地址)或者PTR(获得IP地址对应的域名),类型列表如下:

    表1 DNS报文查询类型

    类型 助记符 说明
    1 A IPv4地址
    2 NS 名字服务器
    5 CNAME 规范名称定义主机的正式名字的别名
    6 SOA 开始授权标记一个区的开始
    11 WKS 熟知服务定义主机提供的网络服务
    12 PTR 指针把IP地址转化为域名
    13 HINFO 主机信息给出主机使用的硬件和操作系统的表述
    15 MX 邮件交换把邮件改变路由送到邮件服务器
    28 AAAA IPv6地址
    252 AXFR 传送整个区的请求
    255 ANY 对所有记录的请求

    NS记录指定了名字服务器。一般情况,每个DNS数据库中,针对每个顶级域都会有一条NS记录,这样一来,电子邮件就可以被发送到域名树中远处的部分。

    查询类

    通常为1,指Internet数据。

    (2)回答、权威、附加区域

    回答区域包含了最初请求名字的资源记录,一个回答报文的回答区域可以包含多条资料记录RR(因为一个主机名可以对应多个IP地址,冗余Web服务器)。权威区域包含了其他权威DNS服务器的记录。附加区域包含其他一些”有帮助”的记录,例如,对于一个MX(邮件交换)请求的回答报文中,回答区域包含一条资料记录(该记录提供邮件服务器的规范主机名),附加区域可以包含一条类型A记录(该记录提供了该邮件服务器的规范主机名的IP地址)。

    每条资料记录是一个五元组,如下:

    (域名,生存期,类别,类型,值)

    直接表示如下[1]:

    图6 DNS报文的资源记录

    域名(2字节或不定长)

    记录中资源数据对应的名字,它的格式和查询名字段格式相同。当报文中域名重复出现时,就需要使用2字节的偏移指针来替换。例如,在资源记录中,域名通常是查询问题部分的域名的重复,就需要用指针指向查询问题部分的域名。关于指针怎么用,TCP/IP详解里面有,即2字节的指针,最前面的两个高位是11,用于识别指针。其他14位从报文开始处计数(从0开始),指出该报文中的相应字节数。注意,DNS报文的第一个字节是字节0,第二个报文是字节1。一般响应报文中,资源部分的域名都是指针C00C(1100000000001100,12正好是首部区域的长度),刚好指向请求部分的域名[1]。

    类型(记录的类型,见表1)

    A记录,Name是主机名,Value是该主机名的IP地址,因此,一条类型为A的资源记录提供了标准的主机名到IP地址的映射。

    NS记录,Name是域(如foo.com),Value是知道如何获得该域中主机IP地址的权威DNS服务器的主机名(如dns.foo.com),这个记录常用于沿着查询链进一步路由DNS查询。

    CNAME记录,Name是主机别名,Value是主机别名对应的规范主机名,该记录能够向请求主机提供一个主机名对应的规范主机名。

    MX记录,Name是邮件服务器别名,Value是邮件服务器别名的规范主机名。通过MX记录,一个公司的邮件服务器和其他服务器可以使用相同的别名。

    注:有着复杂主机名的主机能拥有多个别名,前者称为规范主机名,后者称为主机别名(便于记忆)。

    对于Internet信息,它总是IN。

    生存时间

    用于指示该记录的稳定程度,极为稳定的信息会被分配一个很大的值(如86400,一天的秒数)。该字段表示资源记录的生命周期(以秒为单位),一般用于当地址解析程序取出资源记录后决定保存及使用缓存数据的时间[1]。

    资源数据长度(2字节)

    表示资源数据的长度(以字节为单位,如果资源数据为IP则为0004)。

    资源数据

    该字段是可变长字段,表示按查询段要求返回的相关资源记录的数据。

    2.2 DNS查询报文实例

    www.baidu.com为例,用Wireshark俘获分组,结合2.1的理论内容,很容易看明白的,DNS请求报文如下:

    图7 DNS请求报文示例

    2.3 DNS回答报文实例

    图8 DNS回答报文示例

    转载请注明:爱开源 » Wireshark分析DNS 协议

  • 自己动手实现DNS协议

    自己动手实现DNS协议

    在本博客的DNS协议详解及报文格式分析一文中介绍了DNS的基本理论,DNS协议的报文格式等,如果详细了解了的话,不免会萌生出自己实现DNS协议的想法。要知道DNS协议是基于UDP的,如果能够自己组装出一个合法有效的DNS报文,便可以通过socket将DNS查询报文发出去,并能得到相应的域名服务器的响应报文,对响应报文进行解析,便可以得到最终的IP地址。本文基于此,介绍了实现DNS协议的思路,给出了完整的可运行代码。废话不多说,马上开始。

    1. 协议头部结构定义

    首先需要根据 DNS协议详解及报文格式分析 一文的介绍,将DNS的头部数据结构构造出来。DNS头部实际上只有三个部分的内容:会话标识(2字节),标志(2字节)和数量字段(共8字节),这个头部是最终的发送或是接收报文的头部,所以采用的是网络字节序。下面给出代码。

    #pragma pack(push, 1)
    struct DNSHeader
    {
        /* 1. 会话标识(2字节)*/
        unsigned short usTransID;        // Transaction ID
    
        /* 2. 标志(共2字节)*/
        unsigned char RD : 1;            // 表示期望递归,1bit
        unsigned char TC : 1;            // 表示可截断的,1bit
        unsigned char AA : 1;            // 表示授权回答,1bit
        unsigned char opcode : 4;        // 0表示标准查询,1表示反向查询,2表示服务器状态请求,4bit
        unsigned char QR : 1;            // 查询/响应标志位,0为查询,1为响应,1bit
    
        unsigned char rcode : 4;         // 表示返回码,4bit
        unsigned char zero : 3;          // 必须为0,3bit
        unsigned char RA : 1;            // 表示可用递归,1bit
    
        /* 3. 数量字段(共8字节) */
        unsigned short Questions;        // 问题数
        unsigned short AnswerRRs;        // 回答资源记录数
        unsigned short AuthorityRRs;     // 授权资源记录数
        unsigned short AdditionalRRs;    // 附加资源记录数
    };
    #pragma pack(pop)
    

    上述代码有几个地方需要注意一下:

    • #pragma pack(push, 1)#pragma pack(pop)。使结构体按1字节方式对齐,其中push表示把原来的对齐方式压栈,pop表示恢复原来的对齐方式。
    • usTransID、Questions、AnswerRRs... 这些两个字节的字段,由于是网络字节序,所以在给这些字段填充内容时,需要使用htons 函数做转换,后面的报文组装代码里有写到。
    • 如果仔细对比会发现标志字段的书写顺序比较怪异。比如标志字段的第一个字节,在DNS报文中顺序应该是<QR-opcode-AA-TC-RD>(注:按照报文中内容的顺序,QR是低位,RD是高位),而上面的代码中顺序是 <RD-TC-AA-OPCODE-QR> ,这是因为我们定义各个位时使用了C/C++中的位域语法。位域中将高位放在了前面,将低位放在了后面,比如:1011 0010B,如果用下面的BitFieldDemo 所示的位域结构表示的话,则 a == 10B, b == 110B, c == 010B 。所以,<RD-TC-AA-OPCODE-QR> 这样的定义其实表明RD是高位,QR是低位,正好符合了DNS的头部标志字段要求。标志字段的第二个字节类似。
    struct BitFieldDemo
    {   // 假如有二进制数1011 0010B,左边为高位,右边为低位
         // 则a == 10B, b == 110B, c == 010B
        unsigned char a : 2; // 高2位
        unsigned char b : 3;
        unsigned char c : 3; // 低2位
    };
    

    2. 查询报文组装与发送

    万事开头难,头部数据结构定义好了之后,后面就好办多了,无非就是将标志以及需要查询的内容(主要是域名)填充到头部和正文的Queries字段,然后使用socket发出去即可。完整代码见下面SendDnsPack所示,本节给出的查询报文组装与发送实现代码中,是以A类型(0x1)为例的,A类型表示由域名查询获得IPv4地址。

    主要分为以下两个大的步骤:

    1. 根据第1节定义的DNS报文头部,组装查询报文
    2. 使用sendto函数将报文发送到DNS服务器的53号端口
    // @Brief : 发送DNS查询报文
    // @Param: usID: 报文ID编号
    //                pSocket: 需要发送的socket
    //                szDnsServer: DNS服务器地址
    //                szDomainName: 需要查询的域名
    // @Retrun: true表示发送成功,false表示发送失败
    bool SendDnsPack(IN unsigned short usID,
                     IN SOCKET *pSocket,
                     IN const char *szDnsServer,
                     IN const char *szDomainName)
    {
        bool bRet = false;
    
        if (*pSocket == INVALID_SOCKET
            || szDomainName == NULL
            || szDnsServer == NULL
            || strlen(szDomainName) == 0
            || strlen(szDnsServer) == 0)
        {
            return bRet;
        }
    
        unsigned int uiDnLen = strlen(szDomainName);
    
        // 判断域名合法性,域名的首字母不能是点号,域名的
        // 最后不能有两个连续的点号
        if ('.' == szDomainName[0] || ( '.' == szDomainName[uiDnLen - 1]
              && '.' == szDomainName[uiDnLen - 2])
           )
        {
            return bRet;
        }
    
        /* 1. 将域名转换为符合查询报文的格式 */
        // 查询报文的格式是类似这样的:
        //      6 j o c e n t 2 m e 0
        unsigned int uiQueryNameLen = 0;
        BYTE *pbQueryDomainName = (BYTE *)malloc(uiDnLen + 1 + 1);
        if (pbQueryDomainName == NULL)
        {
            return bRet;
        }
        // 转换后的查询字段长度为域名长度 +2
        memset(pbQueryDomainName, 0, uiDnLen + 1 + 1);
    
        // 下面的循环作用如下:
        // 如果域名为  jocent.me ,则转换成了 6 j o c e n t  ,还有一部分没有复制
        // 如果域名为  jocent.me.,则转换成了 6 j o c e n t 2 m e
        unsigned int uiPos    = 0;
        unsigned int i        = 0;
        for ( i = 0; i < uiDnLen; ++i)
        {
          if (szDomainName[i] == '.')
          {
              pbQueryDomainName[uiPos] = i - uiPos;
              if (pbQueryDomainName[uiPos] > 0)
              {
                  memcpy(pbQueryDomainName + uiPos + 1, szDomainName + uiPos, i - uiPos);
              }
              uiPos = i + 1;
          }
        }
    
        // 如果域名的最后不是点号,那么上面的循环只转换了一部分
        // 下面的代码继续转换剩余的部分, 比如 2 m e
        if (szDomainName[i-1] != '.')
        {
          pbQueryDomainName[uiPos] = i - uiPos;
          memcpy(pbQueryDomainName + uiPos + 1, szDomainName + uiPos, i - uiPos);
          uiQueryNameLen = uiDnLen + 1 + 1;
        }
        else
        {
          uiQueryNameLen = uiDnLen + 1;
        }
        // 填充内容  头部 + name + type + class
        DNSHeader *PDNSPackage = (DNSHeader*)malloc(sizeof(DNSHeader) + uiQueryNameLen + 4);
        if (PDNSPackage == NULL)
        {
            goto exit;
        }
        memset(PDNSPackage, 0, sizeof(DNSHeader) + uiQueryNameLen + 4);
    
        // 填充头部内容
        PDNSPackage->usTransID = htons(usID);  // ID
        PDNSPackage->RD = 0x1;   // 表示期望递归
        PDNSPackage->Questions = htons(0x1);  // 本文第一节所示,这里用htons做了转换
    
        // 填充正文内容  name + type + class
        BYTE* PText = (BYTE*)PDNSPackage + sizeof(DNSHeader);
        memcpy(PText, pbQueryDomainName, uiQueryNameLen);
    
        unsigned short *usQueryType = (unsigned short *)(PText + uiQueryNameLen);
        *usQueryType = htons(0x1);        // TYPE: A
    
        ++usQueryType;
        *usQueryType = htons(0x1);        // CLASS: IN
    
        // 需要发送到的DNS服务器的地址
        sockaddr_in dnsServAddr = {};
        dnsServAddr.sin_family = AF_INET;
        dnsServAddr.sin_port = ::htons(53);  // DNS服务端的端口号为53
        dnsServAddr.sin_addr.S_un.S_addr = ::inet_addr(szDnsServer);
    
        // 将查询报文发送出去
        int nRet = ::sendto(*pSocket,
            (char*)PDNSPackage,
            sizeof(DNSHeader) + uiQueryNameLen + 4,
            0,
            (sockaddr*)&dnsServAddr,
            sizeof(dnsServAddr));
        if (SOCKET_ERROR == nRet)
        {
            printf("DNSPackage Send Fail! \n");
            goto exit;
        }
    
        // printf("DNSPackage Send Success! \n");
        bRet = true;
    
    // 统一的资源清理处
    exit:
        if (PDNSPackage)
        {
            free(PDNSPackage);
            PDNSPackage = NULL;
        }
    
        if (pbQueryDomainName)
        {
            free(pbQueryDomainName);
            pbQueryDomainName = NULL;
        }
    
        return bRet;
    }
    

    代码中有几个地方需要注意一下:

    • 关于域名合法性的判断,域名的开头不能有点号,但是域名的结尾是允许有一个点号的,结尾的点号其实表示的是根域名服务器,在本博客 DNS协议详解及报文格式分析 这篇文章有讲到
    • 因为最终发出的报文中的Queries字段中,查询的名字格式是类似 6 j o c e n t 2 m e 0这样的,所以有一部分代码是做这个转换的
    • Type, Class,Questions 都是两个字节,为了转换成网路字节序,需要使用 htons 函数

    3. 响应报文接收与解析

    当成功的向DNS服务端发出查询报文后,接下来就是等待响应报文,DNS响应报文的格式与查询报文相比,头部标志字段QR由0变成了1,正文部分多了些字段,比如Answers字段等。下文中的RecvDnsPack即是响应报文接收与解析的代码。

    主要分为以下两个大的步骤:

    1. 使用recvfrom 函数接收服务端返回的内容
    2. 解析收到的内容,主要是从中获取IP地址。解析的过程中首先对收到内容的合法性做了一些校验,分别校验了内容长度、ID号、QR值等;然后使用指针遍历的方式依次解析响应报文中的内容

    代码中注释已经相当详细,不再赘述。

    void RecvDnsPack(IN unsigned short usId,
                     IN SOCKET *pSocket )
    {
        if (*pSocket == INVALID_SOCKET)
        {
            return;
        }
    
        char szBuffer[256] = {};        // 保存接收到的内容
        sockaddr_in servAddr = {};
        int iFromLen = sizeof(sockaddr_in);
    
        int iRet = ::recvfrom(*pSocket,
            szBuffer,
            256,
            0,
            (sockaddr*)&servAddr,
            &iFromLen);
        if (SOCKET_ERROR == iRet || 0 == iRet)
        {
            printf("recv fail \n");
            return;
        }
    
        /* 解析收到的内容 */
        DNSHeader *PDNSPackageRecv = (DNSHeader *)szBuffer;
        unsigned int uiTotal       = iRet;        // 总字节数
        unsigned int uiSurplus     = iRet;  // 接受到的总的字节数
    
        // 确定收到的szBuffer的长度大于sizeof(DNSHeader)
        if (uiTotal <= sizeof(DNSHeader))
        {
            printf("接收到的内容长度不合法\n");
            return;
        }
    
        // 确认PDNSPackageRecv中的ID是否与发送报文中的是一致的
        if (htons(usId) != PDNSPackageRecv->usTransID)
        {
            printf("接收到的报文ID与查询报文不相符\n");
            return;
        }
    
        // 确认PDNSPackageRecv中的Flags确实为DNS的响应报文
        if ( 0x01 != PDNSPackageRecv->QR )
        {
            printf("接收到的报文不是响应报文\n");
            return;
        }
    
        // 获取Queries中的type和class字段
        unsigned char *pChQueries = (unsigned char *)PDNSPackageRecv + sizeof(DNSHeader);
        uiSurplus -= sizeof(DNSHeader);
    
        for ( ; *pChQueries && uiSurplus > 0; ++pChQueries, --uiSurplus ) { ; } // 跳过Queries中的name字段
    
        ++pChQueries;
        --uiSurplus;
    
        if ( uiSurplus < 4 )
        {
            printf("接收到的内容长度不合法\n");
            return;
        }
    
        unsigned short usQueryType  = ntohs( *((unsigned short*)pChQueries) );
        pChQueries += 2;
        uiSurplus -= 2;
    
        unsigned short usQueryClass = ntohs( *((unsigned short*)pChQueries) );
        pChQueries += 2;
        uiSurplus -= 2;
    
        // 解析Answers字段
        unsigned char *pChAnswers = pChQueries;
        while (0 < uiSurplus && uiSurplus <= uiTotal)
        {
            // 跳过name字段(无用)
            if ( *pChAnswers == 0xC0 )  // 存放的是指针
            {
                if (uiSurplus < 2)
                {
                    printf("接收到的内容长度不合法\n");
                    return;
                }
                pChAnswers += 2;       // 跳过指针字段
                uiSurplus -= 2;
            }
            else        // 存放的是域名
            {
                // 跳过域名,因为已经校验了ID,域名就不用了
                for ( ; *pChAnswers && uiSurplus > 0; ++pChAnswers, --uiSurplus ) {;}
                pChAnswers++;
                uiSurplus--;
            }
    
            if (uiSurplus < 4)
            {
                printf("接收到的内容长度不合法\n");
                return;
            }
    
            unsigned short usAnswerType = ntohs( *((unsigned short*)pChAnswers) );
            pChAnswers += 2;
            uiSurplus -= 2;
    
            unsigned short usAnswerClass = ntohs( *( (unsigned short*)pChAnswers ) );
            pChAnswers += 2;
            uiSurplus -= 2;
    
            if ( usAnswerType != usQueryType || usAnswerClass != usQueryClass )
            {
                printf("接收到的内容Type和Class与发送报文不一致\n");
                return;
            }
    
            pChAnswers += 4;    // 跳过Time to live字段,对于DNS Client来说,这个字段无用
            uiSurplus -= 4;
    
            if ( htons(0x04) != *(unsigned short*)pChAnswers )
            {
                uiSurplus -= 2;     // 跳过data length字段
                uiSurplus -= ntohs( *(unsigned short*)pChAnswers ); // 跳过真正的length
    
                pChAnswers += 2;
                pChAnswers += ntohs( *(unsigned short*)pChAnswers );
            }
            else
            {
                if (uiSurplus < 6)
                {
                    printf("接收到的内容长度不合法\n");
                    return;
                }
    
                uiSurplus -= 6;
                // Type为A, Class为IN
                if ( usAnswerType == 1 && usAnswerClass == 1)
                {
                    pChAnswers += 2;
    
                    unsigned int uiIP = *(unsigned int*)pChAnswers;
                    in_addr in = {};
                    in.S_un.S_addr = uiIP;
                    printf("IP: %s\n", inet_ntoa(in));
    
                    pChAnswers += 4;
                }
                else
                {
                    pChAnswers += 6;
                }
            }
        }
    }
    

    4. 测试

    本小节给出上述发送函数与接受函数的测试代码,测试的过程中可以用Wireshark抓包看下发包和收包的情况,能够加深理解。测试程序运行结果如右图所示:

    int main( int argc, char* argv[])
    {
        WSADATA wsaData = {};
        if ( 0 != ::WSAStartup(MAKEWORD(2, 2), &wsaData) )
        {
            printf("WSAStartup fail \n");
            return -1;
        }
    
        SOCKET socket = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
        if (INVALID_SOCKET == socket)
        {
            printf("socket fail \n");
            return -1;
        }
    
       int nNetTimeout = 2000;
    
       // 设置发送时限
       ::setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&nNetTimeout, sizeof(int));
       // 设置接收时限
       ::setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&nNetTimeout,sizeof(int));
    
       // 随机生成一个ID
       srand((unsigned int)time(NULL));
       unsigned short usId = (unsigned short)rand();
    
       // 自定义需要查询的域名
       char szDomainName[256] = {};
       printf("输入要查询的域名:");
       scanf("%s", szDomainName);
    
       // 发送DNS报文,因为测试,这里就简单指定8.8.8.8作为查询服务器
       if (!SendDnsPack(usId, &socket, "8.8.8.8", szDomainName))
       {
            return -1;
       }
    
       // 接收响应报文,并显示获得的IP地址
       RecvDnsPack(usId, &socket);
    
       closesocket(socket);
    
       WSACleanup();
       return 0;
    }
    

    P.S. 本文代码所用的编译链接环境是Windows下的VC编译环境,如果在Linux下编译可能需要对代码做略微调整,但整体结构应该是一样的,知悉。

    转载请注明:爱开源 » 自己动手实现DNS协议