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