IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    用 nfqueue + Python 回复 IPv6 DNS 请求

    依云发表于 2016-04-17 21:37:38
    love 0

    本文来自依云's Blog,转载请注明。

    发生了这么一件事:服务器访问某些 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~所以大家还是尽量升级吧,有好处的呢。



沪ICP备19023445号-2号
友情链接