发生了这么一件事:服务器访问某些 URL 时经常会花费好几秒的时间。重现并分析 strace 记录之后,发现是 DNS 的 AAAA 记录的问题。
情况是这样的:CentOS 6 默认启用了 IPv6,于是 glibc 就会同时进行 A 和 AAAA 记录查询。然后呢,上游 DNS 是运营商的,不给力,经常在解析 AAAA 记录时花费几秒甚至十几秒,然后超时或者返回个 SERVFAIL。
不过咱在天朝,也没有 IPv6 网络可用,所以就禁用 IPv6 吧。通过 sysctl 禁用 IPv6 无效。glibc 是通过创建 IPv6 套接字的方式来决定是否进行 AAAA 请求的。查了一下,禁用掉 ipv6 这个内核模块就可以了。可是,rmmod ipv6 报告模块正在使用中。
lsof -nPi | grep -i ipv6
会列出几个正在使用 IPv6 套接字的进程。重启服务,或者重启机器太麻烦了,也不知道会不会有进程会起不来……于是我想起了歪心思:既然是因为 AAAA 记录回应慢,而咱并不使用这个回应,那就及时伪造一个呗。
于是上 nfqueue。DNS 是基于 UDP 的,所以处理起来也挺简单。用的是 netfilterqueue 这个库。DNS 解析用的是 dnslib。
因为 53 端口已经被 DNS 服务器占用了,所以想从这个地址发送回应还得用底层的方法。尝试过 scapy,然而包总是发不出去,Wireshark 显示 MAC 地址没有正确填写……实在弄不明白要怎么做,干脆用 RAW 套接字好了。man 7 raw
之后发现挺简单的嘛,因为它直接就支持 UDP 协议,不用自己处理 IP 头。UDP 头还是要自己处理的,8 字节,其中最麻烦的校验和可以填全零=w= 至于解析 nfqueue 那边过来的 IP 包,我只需要源地址就可以了,所以就直接从相应的偏移取了~(其实想想,好像 dnslib 也不需要呢~)
代码如下(Gist 上也放了一份):
#!/usr/bin/env python3
import socket
import struct
import traceback
import subprocess
import time
import signal
import dnslib
from dnslib import DNSRecord
from netfilterqueue import NetfilterQueue
AAAA = dnslib.QTYPE.reverse['AAAA']
udpsock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP)
PORT = 53
def handle_packet(pkt):
s = time.time()
try:
ip = pkt.get_payload()
# 28 = 20B IPv4 header + 8B UDP header
dns = DNSRecord.parse(ip[28:])
if dns.q.qtype == AAAA:
ret = dns.reply()
src = socket.inet_ntoa(ip[12:16])
sport = struct.unpack('!H', ip[20:22])[0]
p = ret.pack()
# print(ret, p)
checksum = 0
p = struct.pack('!HHHH', PORT, sport, len(p) + 8, checksum) + p
udpsock.sendto(p, (src, sport))
pkt.drop()
else:
pkt.accept()
except KeyboardInterrupt:
pkt.accept()
raise
except Exception:
traceback.print_exc()
pkt.accept()
e = time.time()
print('%.3fms' % ((e - s) * 1000))
def main():
nfqueue = NetfilterQueue()
nfqueue.bind(1, handle_packet)
try:
nfqueue.run()
except KeyboardInterrupt:
print()
def quit(signum, sigframe):
raise KeyboardInterrupt
if __name__ == '__main__':
signal.signal(signal.SIGTERM, quit)
signal.signal(signal.SIGQUIT, quit)
signal.signal(signal.SIGHUP, quit)
subprocess.check_call(['iptables', '-I', 'INPUT', '-p', 'udp', '-m', 'udp', '--dport', str(PORT), '-j', 'NFQUEUE', '--queue-num', '1'])
try:
main()
finally:
subprocess.check_call(['iptables', '-D', 'INPUT', '-p', 'udp', '-m', 'udp', '--dport', str(PORT), '-j', 'NFQUEUE', '--queue-num', '1'])
写好之后、准备部署前,我还担心了一下 Python 的执行效率——要是请求太多处理不过来就麻烦了,得搞多进程呢。看了一下,一个包只有 3ms 的处理时间。然后发现 Python 其实也没有那么慢嘛,绝大部分时候不到 1ms 就搞定了~
部署到咱的 DNS 服务器上之后,AAAA 记录回应迅速,再也不会慢了~
调试网络程序,Wireshark 就是好用!
PS: 后来有人告诉我改 gai.conf 也可以。我试了一下,如下设置并没有阻止 glibc 请求 AAAA 记录——它压根就没读这个文件!
precedence ::ffff:0:0/96 100
PPS: 我还发现发送这两个 DNS 请求,glibc 2.12 用了两次 sendto,但是 Arch Linux 上的 glibc 2.23 只用了一次 sendmmsg~所以大家还是尽量升级吧,有好处的呢。