dnspython是一个处理DNS的Python工具模块,支持查询、DNS动态更新、操作ZONE配置文件等功能。由于网上文档较少且不详细,官方文档还不完善,这个模块使用起来比较困难,所以我决定把我自己学到的东西做个记录总结。

学习环境部署

操作系统:Centos7.4

安装模块:pip install dnspython –upgrade

注:默认安装的模块版本是1.12.0,根据我的测试结果这个模块有些问题,因此加上参数upgrade升级到最新的1.15.0

为了方便测试dnspython模块的功能,我们用bind搭建一个最小化配置的DNS服务器:

1. 安装bind软件包

yum install bind -y

2. 生成TSIG key

TSIG key用于保护DNS主从更新、动态更新等操作,只有通过了认证的同步或更新请求才会被接受。首先用dnssec-keygen命令生成这个key:

[root@localhost dns]# dnssec-keygen -a HMAC-MD5 -b 128 -n HOST "test_key"

Ktest_key.+157+52058

这条命令生成了2个文件Ktest_key.+157+52058.key和Ktest_key.+157+52058.private,

查看Ktest_key.+157+52058.private内容如下:

[root@localhost dns]# cat Ktest_key.+157+52058.private 

Private-key-format: v1.3

Algorithm: 157 (HMAC_MD5)

Key: B6kQalChhELcQKgCwD+UQw==

Bits: AAA=

Created: 20180126091256

Publish: 20180126091256

Activate: 20180126091256

根据Algorithm和Key字段的内容,在/etc/named.conf内添加内容:

key "test_key" {                                    

        algorithm hmac-md5;  

        secret "epYaIl5VMJGRSG4WMeFW5g==";        

};

3. 定义ZONE文件

创建ZONE文件/var/named/apple.tree.zone,定义一个我们自己的测试域apple.tree,内容如下,192.168.183.131是我的测试机的ip,请替换成你们实际环境的ip:

$ORIGIN .

$TTL 86400 ; 1 day

apple.tree IN SOA apple.tree. apple.apple.tree. (

2016090107 ; serial

28800      ; refresh (8 hours)

7200       ; retry (2 hours)

604800     ; expire (1 week)

86400      ; minimum (1 day)

)

NS ns1.apple.tree.

A 192.168.183.131

$ORIGIN apple.tree.

a A 192.168.183.132

b A 192.168.183.133

$TTL 60 ; 1 minute

c CNAME b

ns1 A 192.168.183.131

test A 1.1.1.1

test A 2.2.2.2

在/etc/named.conf中添加这个ZONE,并且只允许通过上面定义的TSIG key验证的客户端动态更新此ZONE:

key "test_key" {

       algorithm hmac-md5;

       secret "epYaIl5VMJGRSG4WMeFW5g==";

};

4. /etc/named.conf最终内容如下:

options {

listen-on port 53 { 127.0.0.1; 192.168.183.131; };

listen-on-v6 port 53 { ::1; };

directory "/var/named";

dump-file "/var/named/data/cache_dump.db";

statistics-file "/var/named/data/named_stats.txt";

memstatistics-file "/var/named/data/named_mem_stats.txt";

allow-query     { any; };

recursion yes;


dnssec-enable yes;

dnssec-validation yes;


bindkeys-file "/etc/named.iscdlv.key";


managed-keys-directory "/var/named/dynamic";


pid-file "/run/named/named.pid";

session-keyfile "/run/named/session.key";

};

key "test_key" {

        algorithm hmac-md5;

        secret " B6kQalChhELcQKgCwD+UQw== ";

};


logging {

        channel default_debug {

                file "data/named.run";

                severity dynamic;

        };

};


zone "." IN {

type hint;

file "named.ca";

};


zone "apple.tree" IN {

    type master;

    file "/var/named/apple.tree.zone";

    allow-update { key test_key; };

};


include "/etc/named.rfc1912.zones";

include "/etc/named.root.key";

5. 重启named服务

systemctl restart named


6. 验证

把DNS服务器改成本机,解析上面添加的ZONE中的记录,如果得到下面的结果,说明DNS服务搭建成功:

[root@localhost dns]# host test.apple.tree

test.apple.tree has address 1.1.1.1

test.apple.tree has address 2.2.2.2


DNS查询

dns.resolver实现了DNS查询功能,类似nslookup命令。具体使用方法:

import dns.resolver


r = dns.resolver.query("test.apple.tree", "A")

print ("qname:",r.qname)

print ("rdtype:",r.rdtype)

for i in r.response.answer:

    for j in i.items:

        print ("address:",j.address)

上面的脚本中我们使用dns.resolver.query方法查询之前添加的关于test.apple.tree的A记录,脚本执行结果如下:

[root@localhost dns]# python query.py 

('qname:', )

('rdtype:', 1)

('address:', u'1.1.1.1')

('address:', u'2.2.2.2')

qname是要查询的域名;rdtype是记录类型,dnspython的数据结构中将各种类型的DNS记录用数字定义,其中“1”表示A记录,具体对应关系如下表:

类型 含义

A 1 主机地址

NS 2 经过授权的名称服务器

CNAME 5 用作别名的规范名称

SOA 6 标记开始一个授权区域

PTR 12 域名指针

MX 15 邮件交换

TXT 16 文本字符串

address是被查询的域名对应的ip地址,可能有多个。

DNS动态更新

dns动态更新是一种在不reload和重启DNS服务的情况下更新ZONE内容的机制。dns.update实现了这种功能。具体用法如下:

import dns.tsigkeyring

import dns.update

import dns.query


keyring = dns.tsigkeyring.from_text({'test_key': 'B6kQalChhELcQKgCwD+UQw=='})

update = dns.update.Update("apple.tree", keyring=keyring)

update.add("test2", 60, 'a', "192.168.183.131")

update.replace("a", 60, 'a', "192.168.183.132")

update.delete("c")

response = dns.query.tcp(update, '127.0.0.1')

print response

在搭建bind的过程中我们生成过一个用于验证的key,动态更新需要用到它,上面脚本中填写的key名字和内容需要跟/etc/named.conf中配置的一样。dns.update.Update类的的方法add、replace和delete分别实现了添加、修改和删除记录的功能。dns.query.tcp向DNS服务器(本例中是127.0.0.1)发送更新请求。执行脚本后查看结果:

[root@localhost dns]# python update.py 

id 58485

opcode UPDATE

rcode NOERROR

flags QR RA

;ZONE

apple.tree. IN SOA

;PREREQ

;UPDATE

;ADDITIONAL

[root@localhost dns]# host test2.apple.tree

test2.apple.tree has address 192.168.183.131

[root@localhost dns]# host a.apple.tree

a.apple.tree has address 192.168.183.132

[root@localhost dns]# host c.apple.tree

Host c.apple.tree not found: 3(NXDOMAIN)

查询结果显示我们脚本中对服务器的操作已经生效了。如果这时候我们去查看ZONE文件,会发现我们的更新没有体现在/var/named/apple.tree.zone中,但是/var/named目录下多出了一个apple.tree.zone.jnl文件,动态更新的内容保存在这个jnl文件中,并被直接加载进内存,当named服务被重启(reload不行)时,更改会被写进/var/named/apple.tree.zone中。

#重启named前

[root@localhost ~]# cat /var/named/apple.tree.zone

$ORIGIN .

$TTL 86400 ; 1 day

apple.tree IN SOA apple.tree. apple.apple.tree. (

2016090107 ; serial

28800      ; refresh (8 hours)

7200       ; retry (2 hours)

604800     ; expire (1 week)

86400      ; minimum (1 day)

)

NS ns1.apple.tree.

A 192.168.183.131

$ORIGIN apple.tree.

a A 192.168.183.132

b A 192.168.183.133

$TTL 60 ; 1 minute

c CNAME b

ns1 A 192.168.183.131

test A 1.1.1.1

test A 2.2.2.2

#重启named后

[root@localhost ~]# cat /var/named/apple.tree.zone

$ORIGIN .

$TTL 86400 ; 1 day

apple.tree IN SOA apple.tree. apple.apple.tree. (

2016090108 ; serial

28800      ; refresh (8 hours)

7200       ; retry (2 hours)

604800     ; expire (1 week)

86400      ; minimum (1 day)

)

NS ns1.apple.tree.

A 192.168.183.131

$ORIGIN apple.tree.

$TTL 60 ; 1 minute

a A 192.168.183.132

$TTL 86400 ; 1 day

b A 192.168.183.133

$TTL 60 ; 1 minute

ns1 A 192.168.183.131

test A 1.1.1.1

A 2.2.2.2

test2 A 192.168.183.131

使用dnspython直接操作ZONE文件

要修改ZONE文件,首先要了解三个概念zone、node和rdataset之间的关系。在一个zone可能包括一个或多个node,一个node包括一个或多个rdataset,一个rdataset包括一组class和type相同的记录数据(rdata)。为了更直观,我们用脚本分析一下zone文件/var/named/apple.tree.zone。

import dns.zone

from dns.exception import DNSException

from dns.rdataclass import *

from dns.rdatatype import *


domain = "apple.tree"

print "Getting zone object for domain", domain

zone_file = "/var/named/apple.tree.zone"


try:

    zone = dns.zone.from_file(zone_file, domain)

    print "Zone origin:", zone.origin

    for name, node in zone.nodes.items():

        rdatasets = node.rdatasets

        print "\n**** BEGIN NODE ****"

        print "node name:", name

        for rdataset in rdatasets:

            print "    --- BEGIN RDATASET ---"

            print "    rdataset string representation:", rdataset

            print "    rdataset rdclass:", rdataset.rdclass

            print "    rdataset rdtype:", rdataset.rdtype

            print "    rdataset ttl:", rdataset.ttl

            print "    rdataset has following rdata:"

            for rdata in rdataset:

                print "        -- BEGIN RDATA --"

                print "        rdata string representation:", rdata

                if rdataset.rdtype == SOA:

                    print "        ** SOA-specific rdata **"

                    print "        expire:", rdata.expire

                    print "        minimum:", rdata.minimum

                    print "        mname:", rdata.mname

                    print "        refresh:", rdata.refresh

                    print "        retry:", rdata.retry

                    print "        rname:", rdata.rname

                    print "        serial:", rdata.serial

                if rdataset.rdtype == MX:

                    print "        ** MX-specific rdata **"

                    print "        exchange:", rdata.exchange

                    print "        preference:", rdata.preference

                if rdataset.rdtype == NS:

                    print "        ** NS-specific rdata **"

                    print "        target:", rdata.target

                if rdataset.rdtype == CNAME:

                    print "        ** CNAME-specific rdata **"

                    print "        target:", rdata.target

                if rdataset.rdtype == A:

                    print "        ** A-specific rdata **"

                    print "        address:", rdata.address

except DNSException, e:

    print e.__class__, e

脚本首先通过dns.zone.from_file方法从文件中读取ZONE信息,把zone文件转化为Python认识的数据结构,然后分层次打印出zone的所有node名称以及node包含的内容,执行结果如下:

[root@localhost dns]# python zone.py 

Getting zone object for domain apple.tree

Zone origin: apple.tree.


**** BEGIN NODE ****

node name: @

    --- BEGIN RDATASET ---

    rdataset string representation: 86400 IN SOA @ apple 2016090108 28800 7200 604800 86400

    rdataset rdclass: 1

    rdataset rdtype: 6

    rdataset ttl: 86400

    rdataset has following rdata:

        -- BEGIN RDATA --

        rdata string representation: @ apple 2016090108 28800 7200 604800 86400

        ** SOA-specific rdata **

        expire: 604800

        minimum: 86400

        mname: @

        refresh: 28800

        retry: 7200

        rname: apple

        serial: 2016090108

    --- BEGIN RDATASET ---

    rdataset string representation: 86400 IN NS ns1

    rdataset rdclass: 1

    rdataset rdtype: 2

    rdataset ttl: 86400

    rdataset has following rdata:

        -- BEGIN RDATA --

        rdata string representation: ns1

        ** NS-specific rdata **

        target: ns1

    --- BEGIN RDATASET ---

    rdataset string representation: 86400 IN A 192.168.183.131

    rdataset rdclass: 1

    rdataset rdtype: 1

    rdataset ttl: 86400

    rdataset has following rdata:

        -- BEGIN RDATA --

        rdata string representation: 192.168.183.131

        ** A-specific rdata **

        address: 192.168.183.131


**** BEGIN NODE ****

node name: a

    --- BEGIN RDATASET ---

    rdataset string representation: 60 IN A 192.168.183.132

    rdataset rdclass: 1

    rdataset rdtype: 1

    rdataset ttl: 60

    rdataset has following rdata:

        -- BEGIN RDATA --

        rdata string representation: 192.168.183.132

        ** A-specific rdata **

        address: 192.168.183.132


**** BEGIN NODE ****

node name: b

    --- BEGIN RDATASET ---

    rdataset string representation: 86400 IN A 192.168.183.133

    rdataset rdclass: 1

    rdataset rdtype: 1

    rdataset ttl: 86400

    rdataset has following rdata:

        -- BEGIN RDATA --

        rdata string representation: 192.168.183.133

        ** A-specific rdata **

        address: 192.168.183.133


**** BEGIN NODE ****

node name: test

    --- BEGIN RDATASET ---

    rdataset string representation: 60 IN A 1.1.1.1

60 IN A 2.2.2.2

    rdataset rdclass: 1

    rdataset rdtype: 1

    rdataset ttl: 60

    rdataset has following rdata:

        -- BEGIN RDATA --

        rdata string representation: 1.1.1.1

        ** A-specific rdata **

        address: 1.1.1.1

        -- BEGIN RDATA --

        rdata string representation: 2.2.2.2

        ** A-specific rdata **

        address: 2.2.2.2


**** BEGIN NODE ****

node name: ns1

    --- BEGIN RDATASET ---

    rdataset string representation: 60 IN A 192.168.183.131

    rdataset rdclass: 1

    rdataset rdtype: 1

    rdataset ttl: 60

    rdataset has following rdata:

        -- BEGIN RDATA --

        rdata string representation: 192.168.183.131

        ** A-specific rdata **

        address: 192.168.183.131


**** BEGIN NODE ****

node name: test2

    --- BEGIN RDATASET ---

    rdataset string representation: 60 IN A 192.168.183.131

    rdataset rdclass: 1

    rdataset rdtype: 1

    rdataset ttl: 60

    rdataset has following rdata:

        -- BEGIN RDATA --

        rdata string representation: 192.168.183.131

        ** A-specific rdata **

        address: 192.168.183.131


从脚本的输出可以看出zone的结构层次。我们的测试域有@、a、b、test、ns1和test2等6个node;其中test2这个node包括1个rdataset,这个rdataset中有一条A记录,指向地址192.168.183.131。

修改zone文件的脚本示例代码如下,在这个脚本中:

import dns.zone,dns.name,dns.rdata


originName = dns.name.from_text('apple.tree')

zone = dns.zone.from_file('/var/named/apple.tree.zone',originName,relativize=False)

print "before modify:"

print zone.to_text()


#增加一条新的A记录

newRdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, "127.0.0.1")

newNode = zone.find_node(dns.name.from_text("mail", originName), create=True)

newRdataset = newNode.find_rdataset(dns.rdataclass.IN, dns.rdatatype.A, create=True)

newRdataset.add(newRdata)


#删除一条已有的A记录

oldRdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, "192.168.183.131")

oldNode = zone.find_node(dns.name.from_text("test2", originName), create=False)

oldRdataset = oldNode.find_rdataset(dns.rdataclass.IN, dns.rdatatype.A, create=False)

oldRdataset.remove(oldRdata)


#修改一条已有的A记录

new_ip = "192.168.183.155"

node = zone.find_node(dns.name.from_text("test", originName), create=False)

Rdataset = node.find_rdataset(dns.rdataclass.IN, dns.rdatatype.A, create=False)

for rdata in Rdataset:

    rdata.address = new_ip


print "after modify:"

print zone.to_text()

脚本增加一条A记录“mail IN A 127.0.0.1”,删除一条A记录“test2 IN A 192.168.183.131”,还把test对应的地址修改为192.168.183.155。dns.zone.to_text方法把zone的信息从Python数据结构转化为字符串,dns.zone.to_text输出的格式跟/var/named/apple.tree.zone不太一样,但也是合法的。脚本执行结果:

[root@localhost dns]# python modify.py 

before modify:

@ 86400 IN SOA @ apple 2016090108 28800 7200 604800 86400

@ 86400 IN NS ns1

@ 86400 IN A 192.168.183.131

a 60 IN A 192.168.183.132

b 86400 IN A 192.168.183.133

ns1 60 IN A 192.168.183.131

test 60 IN A 1.1.1.1

test 60 IN A 2.2.2.2

test2 60 IN A 192.168.183.131


after modify:

@ 86400 IN SOA @ apple 2016090108 28800 7200 604800 86400

@ 86400 IN NS ns1

@ 86400 IN A 192.168.183.131

a 60 IN A 192.168.183.132

b 86400 IN A 192.168.183.133

mail 0 IN A 127.0.0.1

ns1 60 IN A 192.168.183.131

test 60 IN A 192.168.183.155

test 60 IN A 192.168.183.155


把修改后的内容写进/var/named/apple.tree.zone,重启named,测试解析效果:

[root@localhost named]# host mail.apple.tree

mail.apple.tree has address 127.0.0.1

[root@localhost named]# host test.apple.tree

test.apple.tree has address 192.168.183.155

[root@localhost named]# host test2.apple.tree

Host test2.apple.tree not found: 3(NXDOMAIN)

只能修改记录还不算完事,实际环境中的DNS通常都是主从两台,当一个ZONE在主服务器上的数据更新时,会发生一次到从服务器的同步。判断主服务器上的数据是不是更新的依据就是SOA记录中的serial大小,当主的serial大于从时,就会进行同步。因此我们每次编辑完ZONE信息后,都不要忘了增加serial的值,否则就会发生DNS主从数据不一致的情况。在上面的脚本最后增加下面这段代码:

#增加serial

for (name,ttl,rdata) in zone.iterate_rdatas('SOA'):

        rdata.serial += 1