Python DNS缓存服务器,PythonDNS

发表时间:2020-11-16

背景:

因为某些不可知的原因,DNS报文响应很慢或者直接超时,导致网页打开困难,体验很不好。所以想写一个本地DNS缓存,当DNS被解析过一次后,本地存缓存,下次需要再次解析时,直接读取本地缓存,这样可以大大提高解析速度和网页浏览体验。

原理:

本机监听UDP 53端口,模拟DNS服务器。将上网客户端的DNS服务器地址指向本机。当客户端有DNS解析请求时,本机解析DNS报文,用报文中的域名查询本机DNS缓存文件记录,如果无缓存记录,则将报文转发至公网DNS服务器解析,并将解析报文写入缓存文件,然后将解析报文转发给客户端。当下次需要再次解析该域名时,即可直接查询本机缓存文件,直接响应客户端。

待完善:

1、未做定时更新DNS缓存(多次解析的域名,会用最后一次解析成功的响应内容更新)

2、不同类型的DNS解析,不知道是否会有问题,理论上是没问题的,未做深入研究

3、不支持自定义域名解析地址(其实也支持,只是配置很麻烦)

https://gitee.com/setionlee/dns-cache-server

"""
-------------------------------------
# @Time    : 2020/11/6 17:55
# @Author  : setionlee
# @File    : dns_server.py
# @IDE: PyCharm
# TODO: 1、增加定时维护DNS缓存
        2、向服务器查询报文使用异步(DONE)
        3、增加自定义解析支持
--------------------------------------
"""
import socket
import time
import configparser
import struct
import threading
import queue
import pathlib

LOCAL_BIND_ADDR = ('192.168.100.100', 53)  # 本地DNS服务器监听地址:端口
REMOTE_DNS_SERVER_ADDR = ("192.168.100.1", 53)  # 公网互联网DNS服务器地址
DNS_CACHE_FILE = 'dns_cache_new.ini'  # 缓存文件
BUFFER_SIZE = 1024
RESPONSE_TIMEOUT = 3  # 设置DNS服务器响应超时时间
SAVE_TIME = 0  # 统计使用缓存响应节约的时间
# ------------------------------------------#
# 检查配置文件是否存在,不存在则创建
if not pathlib.Path(DNS_CACHE_FILE).exists():
    pathlib.Path(DNS_CACHE_FILE).touch()
# ------------------------------------------#
conf = configparser.ConfigParser()
conf.read(DNS_CACHE_FILE)
# ------------------------------------------#
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(LOCAL_BIND_ADDR)
# ------------------------------------------#
q = queue.Queue()
# ------------------------------------------#
print('DNS cache&proxy server is running...')


def dns_client(client_info, request_package):
    time_init = time.time()  # 统计使用缓存节约的时间
    client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    client.settimeout(RESPONSE_TIMEOUT)
    client.sendto(request_package, REMOTE_DNS_SERVER_ADDR)

    while True:
        try:
            server_response_data, server_info = client.recvfrom(BUFFER_SIZE)
            q.put([client_info, resolve_domain(request_package), server_response_data, time.time() - time_init])
        except socket.timeout:
            print('DNS server response time out')
            q.put([client_info, resolve_domain(request_package), False, time.time() - time_init])
        except ConnectionResetError:
            print('receive DNS server response error: [WinError 10054] 远程主机强迫关闭了一个现有的连接。')
            q.put([client_info, resolve_domain(request_package), False, time.time() - time_init])
        finally:
            break


def opt_cache_file():
    global SAVE_TIME
    while True:
        if not q.empty():
            value = q.get()
            client_info = value[0]
            domain_name = value[1]
            dns_server_response_data = value[2]
            time_diff = value[3]
            if conf.has_section(domain_name):  # 如果缓存记录存在,则更新缓存记录
                SAVE_TIME += time_diff
                conf.set(domain_name, 'query_times', str(query_times + 1))  # 查询记录次数加1
                conf.write(open(DNS_CACHE_FILE, 'w'))  # 如果放在主循环中,特殊情况下因同时操作文件会报错
                response_data_cache = conf.get(domain_name, 'response_data')
                if dns_server_response_data:  # 如果DNS服务器有响应内容
                    dns_srv_rsp_data_list = list(dns_server_response_data)
                    dns_srv_rsp_data_bytes = bytes(dns_srv_rsp_data_list[2:])
                    response_data_str = str(unpack_package(dns_srv_rsp_data_bytes))
                    if response_data_str != response_data_cache:  # 检查响应内容与缓存内容是否一致,不一致则更新缓存
                        print("-" * 100)
                        conf.set(domain_name, 'response_data', response_data_str)
                        conf.set(domain_name, 'update_time', time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
                        conf.write(open(DNS_CACHE_FILE, 'w'))
                        print('domain_name=', domain_name)
                        print('\033[1;31mdns server response was changed, will update response_data\033[0m')
                        print('old_response_data', response_data_cache)
                        print('new_response_data', response_data_str)
                        print("-" * 100)
                print('总共节省%.3f秒' % SAVE_TIME)

            else:  # 如果缓存记录不存在,则新增缓存记录
                if dns_server_response_data:
                    # 将服务器响应报文转为list,去掉transaction_id后将内容写入缓存
                    dns_srv_rsp_list = list(dns_server_response_data)
                    dns_srv_rsp_data = bytes(dns_srv_rsp_list[2:])
                    data_str = str(unpack_package(dns_srv_rsp_data))
                    if not conf.has_section(domain_name):
                        conf.add_section(domain_name)
                        conf.set(domain_name, 'response_data', data_str)
                        conf.set(domain_name, 'create_time', time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
                        conf.set(domain_name, 'query_times', "1")
                        conf.write(open(DNS_CACHE_FILE, 'w'))
                    server.sendto(dns_server_response_data, client_info)
                else:
                    print('DNS server response time out, config could not write')
        time.sleep(0.001)


threading.Thread(target=opt_cache_file).start()


def unpack_package(data):
    return struct.unpack('B'*len(list(data)), data)


def pack_package(data):
    return struct.pack('B'*len(data), *data)


def resolve_domain(request_package):
    """解析DNS查询报文中的域名信息"""
    request_package_list = list(request_package)[12:]  # 去掉头部非域名部分内容
    domain_str = ''
    index = 0
    while index < len(request_package_list):
        data_len = int(request_package_list[index])
        if data_len == 0: break
        for char in request_package_list[index+1:index+1+data_len]:
            domain_str += chr(char)
        domain_str += '.'
        index += data_len+1
    return domain_str[:-1]


while True:
    try:
        data, client_addr = server.recvfrom(BUFFER_SIZE)
        print('-' * 100)
        print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
        print('query_data=', data)
        domain_name = resolve_domain(data)
        print('query domain=', domain_name)
    except ConnectionResetError:
        print('receive dns request error :[WinError 10054] 远程主机强迫关闭了一个现有的连接。')  # 不知道为何会触发该异常
        continue
    try:
        # 将DNS请求报文转成list,并提取transaction_id和查询内容
        recv_data_list = list(data)  # convert receive data to list
        transaction_id = recv_data_list[:2]  # get transaction_id, if use cache, need this var
        data_list = bytes(recv_data_list[2:])  # get dns request package data

        # 尝试去ini文件中读取记录,如果未找到记录则进入异常处理流程(添加记录)
        if conf.has_section(domain_name):
            # 找到记录,使用缓存记录
            response_data = conf.get(domain_name, 'response_data')
            query_times = conf.getint(domain_name, 'query_times')

            # 将缓存记录封装,并发送给请求客户端
            response_data_tmp = response_data.replace('(', '').replace(')', '')  # 去掉记录中的括号
            rsp_data_list = list(map(int, response_data_tmp.split(',')))  # 将配置文件中读取到的缓存数据转为list
            server.sendto(pack_package(transaction_id+rsp_data_list), client_addr)  # 封装后发送给客户端
            print("dns cache found, reply client with cache")
            print("-" * 100)
            threading.Thread(target=dns_client, args=(client_addr, data)).start()  # 向DNS服务器查询,有更新则更新缓存文件
        else:
            # 未查询到缓存记录,新增查询缓存
            print('\033[1;36mnew dns query package, will create new_key\033[0m')
            print("-" * 100)
            threading.Thread(target=dns_client, args=(client_addr, data)).start()
    except Exception as e:
        print('catch Exception: ', e)
    time.sleep(0.001)


文章来源互联网,如有侵权,请联系管理员删除。邮箱:417803890@qq.com / QQ:417803890

微配音

Python Free

邮箱:417803890@qq.com
QQ:417803890

皖ICP备19001818号
© 2019 copyright www.pythonf.cn - All rights reserved

微信扫一扫关注公众号:

联系方式

Python Free