35天:DNS深入 - 递归与迭代查询

第35天:DNS深入 - 递归与迭代查询

“DNS就像互联网的电话簿,将域名翻译成IP地址!”

📚 今日目标

  • 深入理解DNS工作原理
  • 掌握递归查询和迭代查询
  • 学习DNS记录类型
  • 理解DNS缓存机制
  • 实现DNS查询工具

1. DNS基础回顾

1.1 DNS是什么?

DNS (Domain Name System) - 域名系统

作用:将人类可读的域名转换为机器可读的IP地址

示例:
www.example.com  →  93.184.216.34

生活化类比:
DNS = 电话簿
域名 = 人名(张三)
IP地址 = 电话号码(138-xxxx-xxxx)

你记得住"张三",但记不住他的电话号码
DNS帮你把"张三"翻译成具体的电话号码

1.2 为什么需要DNS?

问题:如果没有DNS

访问网站需要记住IP地址:
- 访问百度:39.156.66.10
- 访问谷歌:172.217.160.68
- 访问GitHub:20.205.243.166

缺点:
❌ IP地址难记
❌ IP地址会变化
❌ 无法使用有意义的名称

有了DNS:
✅ 记住域名即可:www.baidu.com
✅ IP变化对用户透明
✅ 域名有意义且易记

2. DNS层级结构

2.1 DNS树状结构

DNS命名空间是一个树状层级结构:

                    根域 (.)
                      |
    ┌─────────────────┼─────────────────┐
    |                 |                 |
   com               org               cn
    |                 |                 |
┌───┴───┐         ┌───┴───┐         ┌───┴───┐
|       |         |       |         |       |
example google   wikipedia ietf    baidu  163
|
www

完整域名(FQDN):www.example.com.
- www: 主机名
- example: 二级域名
- com: 顶级域名(TLD)
- .: 根域(通常省略)

域名层级(从右到左):
1. 根域 (Root): .
2. 顶级域 (TLD): com, org, cn, net
3. 二级域 (SLD): example, google, baidu
4. 子域 (Subdomain): www, mail, ftp

常见顶级域名:
通用顶级域(gTLD):
- .com: 商业
- .org: 组织
- .net: 网络
- .edu: 教育
- .gov: 政府

国家顶级域(ccTLD):
- .cn: 中国
- .us: 美国
- .uk: 英国
- .jp: 日本

2.2 DNS服务器层级

DNS服务器架构:

┌─────────────────────────────────────────┐
│ 根DNS服务器 (Root DNS Servers)          │
│ - 全球13个根服务器集群                   │
│ - 知道所有TLD服务器的位置                │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│ 顶级域DNS服务器 (TLD DNS Servers)       │
│ - .com, .org, .cn等的权威服务器         │
│ - 知道各自域下的权威服务器位置           │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│ 权威DNS服务器 (Authoritative Servers)   │
│ - 存储域名的实际DNS记录                  │
│ - example.com的权威服务器                │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│ 本地DNS服务器 (Local/Recursive Servers) │
│ - ISP提供或自己配置                      │
│ - 执行实际的DNS查询                      │
│ - 缓存查询结果                           │
└─────────────────────────────────────────┘

3. DNS查询类型

3.1 递归查询(Recursive Query)

递归查询流程:

客户端只需要问一次,DNS服务器负责完成所有查询

┌──────────┐
│  客户端  │
└────┬─────┘
     │ 1. 查询 www.example.com
     ↓
┌────────────────┐
│ 本地DNS服务器  │ ← 递归查询的核心
└────┬───────────┘
     │ 2. 查询根服务器
     ↓
┌────────────────┐
│  根DNS服务器   │
└────┬───────────┘
     │ 3. 返回.com服务器地址
     ↓
┌────────────────┐
│ 本地DNS服务器  │
└────┬───────────┘
     │ 4. 查询.com服务器
     ↓
┌────────────────┐
│ .com DNS服务器 │
└────┬───────────┘
     │ 5. 返回example.com服务器地址
     ↓
┌────────────────┐
│ 本地DNS服务器  │
└────┬───────────┘
     │ 6. 查询example.com服务器
     ↓
┌─────────────────────┐
│ example.com服务器   │
└────┬────────────────┘
     │ 7. 返回www的IP地址
     ↓
┌────────────────┐
│ 本地DNS服务器  │
└────┬───────────┘
     │ 8. 返回最终答案
     ↓
┌──────────┐
│  客户端  │ ← 得到IP地址
└──────────┘

特点:
✅ 客户端简单,只问一次
✅ 服务器负责所有查询工作
✅ 结果可以被缓存
❌ 服务器负担重

生活化类比:
递归查询 = 找秘书帮忙
你:"帮我查一下张三的电话"
秘书:"好的,我帮你查"
秘书查遍所有通讯录,然后告诉你结果

3.2 迭代查询(Iterative Query)

迭代查询流程:

每次查询返回下一步应该问谁

┌──────────┐
│  客户端  │ (本地DNS服务器对客户端是递归)
└────┬─────┘
     │ 1. 查询 www.example.com
     ↓
┌────────────────┐
│ 本地DNS服务器  │ ← 从这里开始是迭代查询
└────┬───────────┘
     │ 2. 查询根服务器
     ↓
┌────────────────┐
│  根DNS服务器   │
└────┬───────────┘
     │ 3. 返回:"我不知道,但你可以问.com服务器(192.x.x.x)"
     ↓
┌────────────────┐
│ 本地DNS服务器  │
└────┬───────────┘
     │ 4. 查询.com服务器(192.x.x.x)
     ↓
┌────────────────┐
│ .com DNS服务器 │
└────┬───────────┘
     │ 5. 返回:"我不知道,但你可以问example.com服务器(93.x.x.x)"
     ↓
┌────────────────┐
│ 本地DNS服务器  │
└────┬───────────┘
     │ 6. 查询example.com服务器(93.x.x.x)
     ↓
┌─────────────────────┐
│ example.com服务器   │
└────┬────────────────┘
     │ 7. 返回:"www.example.com的IP是93.184.216.34"
     ↓
┌────────────────┐
│ 本地DNS服务器  │
└────┬───────────┘
     │ 8. 返回最终答案
     ↓
┌──────────┐
│  客户端  │
└──────────┘

特点:
✅ 每个服务器只负责回答或指引
✅ 服务器负担轻
❌ 需要多次查询
❌ 客户端需要多次请求

生活化类比:
迭代查询 = 问路
你:"请问图书馆怎么走?"
路人甲:"我不知道,但你可以问前面的警察"
你走到警察处:"请问图书馆怎么走?"
警察:"直走500米左转"

3.3 查询对比

实际DNS查询过程:

客户端 → 本地DNS: 递归查询
本地DNS → 其他DNS: 迭代查询

为什么这样设计?
1. 简化客户端
2. 减轻根服务器负担
3. 利用本地DNS缓存

完整查询示例:

时间线:
T0: 客户端请求 www.example.com
    ├─ 客户端 → 本地DNS [递归查询]

T1: 本地DNS开始工作
    ├─ 检查缓存(未命中)
    ├─ 本地DNS → 根服务器 [迭代查询]
    └─ 根返回:请查询.com服务器(a.gtld-servers.net)

T2: 本地DNS继续
    ├─ 本地DNS → .com服务器 [迭代查询]
    └─ .com返回:请查询ns1.example.com

T3: 本地DNS最后查询
    ├─ 本地DNS → ns1.example.com [迭代查询]
    └─ ns1返回:93.184.216.34

T4: 结果返回
    ├─ 本地DNS缓存结果(TTL=300秒)
    └─ 本地DNS → 客户端 [返回答案]

总耗时:约50-100ms(首次查询)

4. DNS记录类型

4.1 常见DNS记录

DNS资源记录(Resource Records, RR)格式:
名称  TTL  类  类型  数据

主要记录类型:

1. A记录 (Address)
   - 域名 → IPv4地址
   - 示例:www.example.com. 300 IN A 93.184.216.34
   - 用途:最常用,将域名指向IPv4

2. AAAA记录
   - 域名 → IPv6地址
   - 示例:www.example.com. 300 IN AAAA 2606:2800:220:1:248:1893:25c8:1946
   - 用途:将域名指向IPv6

3. CNAME记录 (Canonical Name)
   - 域名 → 另一个域名(别名)
   - 示例:www.example.com. 300 IN CNAME example.com.
   - 用途:创建域名别名
   - 注意:CNAME不能与其他记录共存

4. MX记录 (Mail Exchange)
   - 域名 → 邮件服务器
   - 示例:example.com. 300 IN MX 10 mail.example.com.
   - 优先级:数字越小优先级越高
   - 用途:指定邮件服务器

5. NS记录 (Name Server)
   - 域名 → 权威DNS服务器
   - 示例:example.com. 300 IN NS ns1.example.com.
   - 用途:指定域名的DNS服务器

6. TXT记录
   - 域名 → 文本信息
   - 示例:example.com. 300 IN TXT "v=spf1 include:_spf.example.com ~all"
   - 用途:SPF、DKIM、域名验证等

7. PTR记录 (Pointer)
   - IP地址 → 域名(反向解析)
   - 示例:34.216.184.93.in-addr.arpa. 300 IN PTR www.example.com.
   - 用途:IP反向查询

8. SOA记录 (Start of Authority)
   - 域的权威信息
   - 包含:主DNS服务器、管理员邮箱、序列号、刷新时间等
   - 示例:
     example.com. 3600 IN SOA ns1.example.com. admin.example.com. (
       2024010601 ; 序列号
       3600       ; 刷新时间
       1800       ; 重试时间
       604800     ; 过期时间
       86400      ; 最小TTL
     )

9. SRV记录 (Service)
   - 服务位置记录
   - 示例:_http._tcp.example.com. 300 IN SRV 10 5 80 www.example.com.
   - 用途:指定服务的位置

4.2 DNS记录示例

完整的DNS区域文件示例:

; example.com DNS区域文件

; SOA记录
example.com.    3600    IN  SOA ns1.example.com. admin.example.com. (
                            2024010601  ; Serial
                            3600        ; Refresh
                            1800        ; Retry
                            604800      ; Expire
                            86400 )     ; Minimum TTL

; NS记录
example.com.    3600    IN  NS  ns1.example.com.
example.com.    3600    IN  NS  ns2.example.com.

; A记录
example.com.    300     IN  A   93.184.216.34
www.example.com. 300    IN  A   93.184.216.34
mail.example.com. 300   IN  A   93.184.216.35
ftp.example.com.  300   IN  A   93.184.216.36

; AAAA记录
www.example.com. 300    IN  AAAA 2606:2800:220:1:248:1893:25c8:1946

; MX记录
example.com.    300     IN  MX  10 mail.example.com.
example.com.    300     IN  MX  20 mail2.example.com.

; CNAME记录
blog.example.com. 300   IN  CNAME www.example.com.
shop.example.com. 300   IN  CNAME www.example.com.

; TXT记录
example.com.    300     IN  TXT "v=spf1 mx ~all"
_dmarc.example.com. 300 IN  TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com"

记录解读:
- TTL 300: 缓存5分钟
- TTL 3600: 缓存1小时
- IN: Internet类
- 优先级10 < 20: mail.example.com优先

5. DNS缓存机制

5.1 缓存层级

DNS缓存的多个层级:

1. 浏览器缓存
   - 最快,但容量小
   - Chrome: chrome://net-internals/#dns
   - TTL: 通常60秒

2. 操作系统缓存
   - Windows: ipconfig /displaydns
   - Linux: systemd-resolved
   - macOS: dscacheutil -cachedump

3. 本地DNS服务器缓存
   - ISP的DNS服务器
   - 或自己配置的DNS(如8.8.8.8)
   - TTL: 由记录的TTL决定

4. 其他DNS服务器缓存
   - 中间DNS服务器
   - 根据TTL缓存

缓存工作流程:
查询 www.example.com

Level 1: 浏览器缓存
  ├─ 命中 → 直接返回 ✅
  └─ 未命中 → 继续

Level 2: OS缓存
  ├─ 命中 → 返回 ✅
  └─ 未命中 → 继续

Level 3: 本地DNS缓存
  ├─ 命中 → 返回 ✅
  └─ 未命中 → 开始递归/迭代查询

Level 4: 实际DNS查询
  └─ 查询权威服务器 → 获取答案 → 各层缓存

5.2 TTL(生存时间)

TTL (Time To Live) - 缓存有效期

示例:
www.example.com. 300 IN A 93.184.216.34
                 ↑
                TTL=300秒(5分钟)

TTL的作用:
1. 控制缓存时间
2. 平衡性能和新鲜度
3. 减轻DNS服务器负担

TTL设置策略:

短TTL(60-300秒):
✅ 变更生效快
✅ 适合频繁变更的记录
❌ 查询多,服务器压力大

长TTL(3600-86400秒):
✅ 减少查询,降低延迟
✅ 减轻服务器负担
❌ 变更生效慢

推荐设置:
- 常用记录(www): 300-3600秒
- 稳定记录(NS): 3600-86400秒
- 邮件记录(MX): 3600秒
- 变更前临时降低TTL

6. 实战项目:DNS查询工具

6.1 代码实现

# day35_dns_query_tool.py
# 功能:DNS查询工具

import socket
import struct
from typing import List, Tuple, Dict
import time


class DNSQuery:
    """
    DNS查询工具

    功能:
        1. 构造DNS查询报文
        2. 发送查询
        3. 解析响应
        4. 支持多种记录类型
    """

    # DNS记录类型
    QTYPE = {
        'A': 1,      # IPv4地址
        'NS': 2,     # 域名服务器
        'CNAME': 5,  # 规范名称
        'SOA': 6,    # 授权开始
        'PTR': 12,   # 指针记录
        'MX': 15,    # 邮件交换
        'TXT': 16,   # 文本
        'AAAA': 28,  # IPv6地址
        'ANY': 255   # 所有记录
    }

    def __init__(self, dns_server='8.8.8.8', port=53):
        """
        初始化DNS查询器

        参数:
            dns_server: DNS服务器地址
            port: DNS端口(默认53)
        """
        self.dns_server = dns_server
        self.port = port

    def build_query(self, domain: str, qtype: str = 'A') -> bytes:
        """
        构造DNS查询报文

        参数:
            domain: 要查询的域名
            qtype: 查询类型(A, AAAA, MX等)

        返回:
            DNS查询报文(字节)

        DNS报文格式:
        +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        |                    Header                     |
        +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        |                   Question                    |
        +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        |                    Answer                     |
        +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        |                  Authority                    |
        +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        |                  Additional                   |
        +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
        """
        # 1. 构造Header(12字节)
        transaction_id = 0x1234  # 事务ID
        flags = 0x0100  # 标准查询,期望递归
        questions = 1   # 问题数
        answers = 0     # 回答数
        authority = 0   # 权威记录数
        additional = 0  # 附加记录数

        header = struct.pack(
            '!HHHHHH',
            transaction_id, flags,
            questions, answers, authority, additional
        )

        # 2. 构造Question
        # 域名编码:将 www.example.com 编码为 \x03www\x07example\x03com\x00
        qname = b''
        for part in domain.split('.'):
            qname += bytes([len(part)]) + part.encode()
        qname += b'\x00'  # 结束标记

        qtype_value = self.QTYPE.get(qtype.upper(), 1)
        qclass = 1  # IN (Internet)

        question = qname + struct.pack('!HH', qtype_value, qclass)

        return header + question

    def parse_response(self, response: bytes) -> Dict:
        """
        解析DNS响应

        参数:
            response: DNS响应报文

        返回:
            解析后的结果字典
        """
        result = {
            'header': {},
            'questions': [],
            'answers': [],
            'authority': [],
            'additional': []
        }

        # 解析Header
        header = struct.unpack('!HHHHHH', response[:12])
        result['header'] = {
            'id': header[0],
            'flags': header[1],
            'questions': header[2],
            'answers': header[3],
            'authority': header[4],
            'additional': header[5]
        }

        # 解析Answer部分(简化版本)
        offset = 12

        # 跳过Question部分
        while response[offset] != 0:
            offset += 1
        offset += 5  # 跳过结束符和QTYPE、QCLASS

        # 解析Answer
        for _ in range(result['header']['answers']):
            # 简化:直接提取IP地址(假设是A记录)
            if offset + 12 <= len(response):
                # 跳过名称(指针)
                offset += 2

                # 读取类型、类、TTL、数据长度
                rtype, rclass, ttl, rdlength = struct.unpack(
                    '!HHIH',
                    response[offset:offset+10]
                )
                offset += 10

                # 读取数据
                rdata = response[offset:offset+rdlength]
                offset += rdlength

                if rtype == 1 and rdlength == 4:  # A记录
                    ip = '.'.join(str(b) for b in rdata)
                    result['answers'].append({
                        'type': 'A',
                        'ttl': ttl,
                        'data': ip
                    })

        return result

    def query(self, domain: str, qtype: str = 'A', verbose: bool = True) -> Dict:
        """
        执行DNS查询

        参数:
            domain: 域名
            qtype: 查询类型
            verbose: 是否显示详细信息

        返回:
            查询结果
        """
        if verbose:
            print(f"\n正在查询: {domain} ({qtype}记录)")
            print(f"DNS服务器: {self.dns_server}:{self.port}")

        # 构造查询
        query_data = self.build_query(domain, qtype)

        # 创建UDP socket
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.settimeout(3)

        try:
            # 发送查询
            start_time = time.time()
            sock.sendto(query_data, (self.dns_server, self.port))

            # 接收响应
            response, _ = sock.recvfrom(512)
            query_time = (time.time() - start_time) * 1000

            # 解析响应
            result = self.parse_response(response)
            result['query_time'] = query_time

            if verbose:
                print(f"查询时间: {query_time:.2f}ms")
                print(f"回答数量: {result['header']['answers']}")

                if result['answers']:
                    print("\n结果:")
                    for answer in result['answers']:
                        print(f"  类型: {answer['type']}")
                        print(f"  TTL: {answer['ttl']}秒")
                        print(f"  数据: {answer['data']}")

            return result

        except socket.timeout:
            if verbose:
                print("❌ 查询超时")
            return {'error': 'timeout'}
        except Exception as e:
            if verbose:
                print(f"❌ 查询失败: {e}")
            return {'error': str(e)}
        finally:
            sock.close()

    def query_with_system(self, domain: str, verbose: bool = True):
        """
        使用系统DNS查询(对比)

        参数:
            domain: 域名
            verbose: 是否显示详细信息
        """
        if verbose:
            print(f"\n使用系统DNS查询: {domain}")

        try:
            start_time = time.time()
            result = socket.getaddrinfo(domain, None)
            query_time = (time.time() - start_time) * 1000

            if verbose:
                print(f"查询时间: {query_time:.2f}ms")
                print("\n结果:")
                for item in result:
                    if item[0] == socket.AF_INET:  # IPv4
                        print(f"  IPv4: {item[4][0]}")
                    elif item[0] == socket.AF_INET6:  # IPv6
                        print(f"  IPv6: {item[4][0]}")

            return {'results': result, 'query_time': query_time}

        except Exception as e:
            if verbose:
                print(f"❌ 查询失败: {e}")
            return {'error': str(e)}


def compare_dns_servers():
    """对比不同DNS服务器的性能"""
    print("\n" + "="*60)
    print(" DNS服务器性能对比")
    print("="*60)

    dns_servers = [
        ('Google DNS', '8.8.8.8'),
        ('Cloudflare DNS', '1.1.1.1'),
        ('114 DNS', '114.114.114.114'),
    ]

    test_domains = [
        'www.baidu.com',
        'www.google.com',
        'www.github.com'
    ]

    print(f"\n测试域名: {', '.join(test_domains)}\n")

    for name, server in dns_servers:
        print(f"\n{name} ({server}):")
        print("-" * 60)

        query_tool = DNSQuery(server)
        total_time = 0
        success_count = 0

        for domain in test_domains:
            result = query_tool.query(domain, verbose=False)
            if 'query_time' in result:
                total_time += result['query_time']
                success_count += 1
                print(f"  {domain}: {result['query_time']:.2f}ms")

        if success_count > 0:
            avg_time = total_time / success_count
            print(f"\n  平均查询时间: {avg_time:.2f}ms")


def main():
    """主程序"""
    print("""
    ╔══════════════════════════════════════════════════════════╗
    ║       DNS查询工具 v1.0                                    ║
    ║       DNS Query Tool                                     ║
    ╚══════════════════════════════════════════════════════════╝
    """)

    # 创建DNS查询器
    dns_query = DNSQuery(dns_server='8.8.8.8')

    # 演示1:基本查询
    print("\n【演示1:基本DNS查询】")
    dns_query.query('www.baidu.com', 'A')

    input("\n按Enter继续...")

    # 演示2:系统查询对比
    print("\n【演示2:自定义vs系统DNS查询对比】")
    dns_query.query_with_system('www.baidu.com')

    input("\n按Enter继续...")

    # 演示3:DNS服务器性能对比
    print("\n【演示3:不同DNS服务器性能对比】")
    compare_dns_servers()

    print("""
\n总结:
DNS查询过程包括:
1. 构造查询报文
2. 发送UDP查询(端口53)
3. 接收响应
4. 解析结果

优化建议:
- 使用快速DNS服务器
- 合理设置TTL
- 启用DNS缓存
- 考虑使用DNS over HTTPS (DoH)

明天我们将学习DNS安全(DNSSEC)!
    """)


if __name__ == "__main__":
    main()