4
17
2016
6

用 nfqueue + Python 回复 IPv6 DNS 请求

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

Category: 网络 | Tags: python DNS iptables 中国特色
11
10
2012
22

dnscrypt + dnsmasq

安装过程没什么特别的就不说了。

启动 dnscrypt-proxy。由于 systemd 实在太复杂,一时半会搞不明白,所以我直接手动启动了:

sudo dnscrypt-proxy --local-address=0.0.0.0:35535 --user=nobody --logfile=/var/log/dnscrypt.log --daemonize

然后测试下:

>>> dig -p 35535 twitter.com @localhost

; <<>> DiG 9.9.2 <<>> -p 35535 twitter.com @localhost
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 65004
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 8192
;; QUESTION SECTION:
;twitter.com.                   IN      A

;; ANSWER SECTION:
twitter.com.            16      IN      A       199.59.148.82
twitter.com.            16      IN      A       199.59.149.230
twitter.com.            16      IN      A       199.59.150.39

;; Query time: 267 msec
;; SERVER: 127.0.0.1#35535(127.0.0.1)
;; WHEN: Sat Nov 10 15:07:01 2012
;; MSG SIZE  rcvd: 88

It works!

然后配置 dnsmasq。

server=127.0.0.1#35535
bogus-nxdomain=67.215.65.132

bogus-nxdomain那行是为 OpenDNS 加的,类似于国内 ISP 的「114 页面」。dnscrypt 默认使用 OpenDNS 的服务器,暂时不知道其它提供加密 DNS 的服务器地址,所以只好这样了。

然后重启 dnsmasq 就 OK 了。

说点题外话。其实很多服务进程启动起来很简单,比如 sshd,改好配置文件后直接运行就可以了。再比如 mongod 和 privoxy,手动启动还方便指定配置文件什么的。我的个人电脑不是服务器,不需要复杂的各种管理。虽然说不是服务器,但重启次数还是不需要和 Windows 比的。所以,systemd 给我带来了一堆麻烦,却没能解决我遇到的任何问题

Category: 网络 | Tags: DNS dnsmasq dnscrypt 安全
12
15
2010
4

从UDP到解决DNS污染的脚本

最近做网络课的实验,涉及UDP协议。因为UDP协议比TCP用得少,所以我以前没试过创建几个UDP的socket。现在忽然有了兴致,就试了试。

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

socket创建好了,往哪里发消息呢?我想到了DNS。首先要有报文。没找到容易上手的Python库,就Google到了这个,DNS报文的格式。没功夫细细研究,先弄个报文测试下:

q = b'>:\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07twitter\x03com\x00\x00\x01\x00\x01'
s.sendto(q, ('8.8.8.8', 53))

\x07twitter\x03com\x00就是要查询的域名了,我想测试什么,明白人都看得出来了。接下来接收回答,接收了多次:

>>> '.'.join(str(int(i)) for i in s.recv(4096)[-4:])
'46.82.174.68'
>>> '.'.join(str(int(i)) for i in s.recv(4096)[-4:])
'128.121.146.100'

再接收的话就阻塞了。对于普通域名,当然是最多只会接收到一次的啦。如果报文出错的话(比如我不小心少写了最后的\x00\x01),是收不到回答的。

有人依据此现象写了个pydnsproxy 放在Google Code上。主页说“暂不公布方法”,但其实很简单。下面是我整理过的代码,转成了Python3的语法:

#!/usr/bin/env python3
# vim:fileencoding=utf-8

from socketserver import *
from socket import *
import sys, os

'''
来源 http://code.google.com/p/pydnsproxy/
'''

DEF_LOCAL_HOST = '127.0.0.1'
DEF_REMOTE_SERVER = '8.8.8.8'
DEF_PORT = 5350
DEF_CONF_FILE = 'dnsserver.conf'
DEF_TIMEOUT = 0.4

gl_remote_server = None

class LocalDNSHandler(BaseRequestHandler):
  def setup(self):
    global gl_remote_server
    if not gl_remote_server:
      remote_server = DEF_REMOTE_SERVER
    else:
      remote_server = gl_remote_server
    self.dnsserver = (remote_server, 53)

  def handle(self):
    data, socket = self.request
    rspdata = self._getResponse(data)
    socket.sendto(rspdata, self.client_address)

  def _getResponse(self, data):
    "Send client's DNS request (data) to remote DNS server, and return its response."
    sock = socket(AF_INET, SOCK_DGRAM) # socket for the remote DNS server
    sock.sendto(data, self.dnsserver)
    sock.settimeout(5)
    while True:
      try:
        rspdata = sock.recv(4096)
        break
      except error as e:
        if e.errno != 11:
          raise
        else:
          print("Try again")
    # "delicious food" for GFW:
    while True:
      sock.settimeout(DEF_TIMEOUT)
      try:
        rspdata = sock.recv(4096)
        print("GFWed?")
      except timeout:
        break
      except error as e:
        if e.errno != 11:
          raise
        else:
          print("Trying again")
    return rspdata

class LocalDNSServer(ThreadingUDPServer):
  pass

def main():
  global gl_remote_server
  try:
    if hasattr(sys, 'frozen'):
      dir = os.path.dirname(sys.executable)
    else:
      dir = os.path.dirname(__file__)
    confFile = os.path.join(dir, DEF_CONF_FILE)
    f = open(confFile, 'r')
    dns = f.read().split('=')
    f.close()
    if len(dns) == 2:
      if dns[0].strip().lower() == 'dns':
        gl_remote_server = dns[1].strip()
      else:
        pass
  except:
    pass
  dnsserver = LocalDNSServer((DEF_LOCAL_HOST, DEF_PORT), LocalDNSHandler)
  dnsserver.serve_forever()

if __name__ == '__main__':
  main()

注意到和原程序不同的是,我捕获了错误号为11的socket.error异常。这个是EAGAIN,“资源临时不可用”,只会在设置了超时后出现。man文档recv(2)对此的解释是:

EAGAIN or EWOULDBLOCK
       The socket is marked  nonblocking  and  the  receive  operation
       would  block, or a receive timeout had been set and the timeout
       expired before data was received.  POSIX.1-2001  allows  either
       error  to be returned for this case, and does not require these
       constants to have the same value,  so  a  portable  application
       should check for both possibilities.

不知道是怎么回事,再次接收却又可以收到数据。也许正如其名,是期待调用者再次尝试吧。我越来越觉得,Python 的异常处理里应该有tryagain这样的语句了。

2012年11月10日更新:更好地摆脱 DNS 污染,请参考此文

Category: 网络 | Tags: DNS python 网络 UDP

部分静态文件存储由又拍云存储提供。 | Theme: Aeros 2.0 by TheBuckmaker.com