[!warning]说明
由于我对于本站征文投票机制和倾向表达的不满,多次引发某些人群对于我“规则利己主义”的批评,认为我不满于规则没有向自己倾斜。因此我决定不再参与后续的任何征文活动,以便彻底站在利益无关方的立场安静地写作,清理这些阴暗的揣测。我一直相信,触及底层原理的文章,尽管因为晦涩且无法即时起效,不被大多数人看好,但它的价值是长存的,它永远是你在迷茫困惑时的一盏灯塔。
所以我不满足于“给你一个Docker-compose配置”,我要为你讲清楚Docker的基础知识;我不会仅限于告诉你怎样一步一步配置域名,我要让大家了解它到底是怎样工作的。
授人以鱼不如授人以渔。我相信即使不参与征文竞赛,没有社区推流和那几十美元的奖金,我的文章也总是能够被知音发掘,并在很长的一段时间里都能为有需要的人带来帮助。
本文考证时间约14天,写作时间约2小时
前言
当你在浏览器输入一个域名时,是否有思考过,计算机是如何通过域名查询到服务器IP地址的?全球无数的主机、无数的域名、无数的查询请求,又是如何维持高度的稳定性与可信度的?在这背后,正是域名系统(Domain Name System)这一大互联网基础设施发挥着作用。
域名的构成
域名是一个倒置的树状结构,从右向左,层级逐渐细化
我们常见的域名写法是www.example.com,然而更加标准的写法应当在末尾增加一个点:
www.example.com.
这就是完全限定域名(FQDN, Fully Qualified Domain Name)
[!note]
是的,在Chrome的地址栏里输入www.example.com.,也可以访问到正确的站点。你可以理解为
www.example.com就像是文件系统里的一个相对路径com/example/www,只是系统默认了这个相对路径以根目录/(在域名系统里叫做搜索域)为参照。而www.example.com.相当于直接输入/com/example/www这个绝对路径。
让我们从右往左分析其结构:
| 区域 | 级别 | 作用 |
|---|---|---|
. |
根域(ROOT) | 整个域名系统的起点 |
com. |
顶级域(TLD, Top-Level Domain) | com由Verisign负责;TLD分为gTLD(通用顶级域)与ccTLD(国家/地区顶级域) |
example.com. |
二级域(Second-Level Domain) | 由组织或个人持有,需要通过注册商购买 |
www.example.com. |
子域 | 可自由创建的前缀 |
全球根与权威名称服务器
全球共有13组根服务器(A~M),它们是DNS查询的最高权威。话虽如此,这13“台”服务器实际上通过任播(Anycast)技术,对应着全球数百个镜像节点,中国虽然没有根服务器(至于“雪人计划”我们后面再聊,新坑+1),但有镜像节点。
全球根服务器对根域(ROOT),也就是我们上文提到的.负责。它管理所有的TLD,并指向它们各自的专门的权威服务器,包括com.,net.,cn.,international.等等。
com./net.会被指向Verisign,而cn.则会被指向CNNIC。
[!note]
现在,你可以打开电脑,输入以下命令,查看它们究竟指向了哪里。dig com. NS +short dig net. NS +short dig cn. NS +short
DNS迭代查询流程
以www.example.com.的查询流程为例,我们需要查询这个域名的A记录,就需要向名称服务器(NS, Name Server)发起请求。
但很遗憾,你现在唯一知道的,就是那13个刻在大脑系统里的全球根服务器。你决定向它们之一求助。
[!note] 用于记录这13个全球根服务器IP的文件,就是根提示文件(Root Hints File)
于是你向198.41.0.4,也就是其中一个全球根发起了请求:
- 我想知道
www.example.com.的A记录
全球根服务器表示很为难:
- 我并不知道
www.example.com.的A记录是什么,不过com.确实归我管,我刚才查询了我的区域(Zone)文件,你现在可以去找com.的NS,它的域名是a.gtld-server.net,它的IP是192.5.6.30
[!note]
a.gtld-server.net是威瑞信(Verisign)的一个权威名称服务器。
这里会同时返回一个IP,叫做“胶水记录”,毕竟.net的NS也是a.gtld-server.net,要是不直接给的IP,那就会出现“想要知道a.gtld-server.net在哪,得去问a.gtld-server.net,至于a.gtld-server.net在哪得问a.gtld-server.net”的抽象问题。
言归正传,你决定遵从全球根服务器的信息,向a.gtld-server.net询问:
- 我想知道
www.example.com.的A记录
Verisign也表示很为难:
- 我也不知道
www.example.com.的A记录是什么,但是我管理着example.com.。com.的区域文件提示,example.com.的NS记录是a.iana-servers.net.
于是你继续找a.iana-servers.net.询问:
- 我想知道
www.example.com.的A记录
这下对了!IANA的名称服务器表示:
- 我确实管理着
example.com.的区域文件,www子域的A记录是95.101.27.204
就这样,经过多次迭代,你终于找到了www.example.com指向的ipv4地址。
[!note]
实际上,DNS服务器会递归地帮助你直接完成上述迭代,你只需要向某个DNS服务器发起委派查询,它会自动、高效地帮助你完成上述过程(如果它没有缓存这个域名的记录),这并不需要你的个人电脑向多个NS发起多次查询。这也回答了另一个问题:我用中国移动的DNS服务器上网,解析托管在阿里云DNS的域名,会比解析托管在Cloudflare的域名更快吗?答案是基本没有区别。
你发现了吗?无论是
.根域,还是com.TLD、example.com.这个你拿到手的SLD,都通过一个预设的NS记录指向其本身以及其子域名的托管服务器。而CNAME则是“优先级高于一切”的别名,这意味着它天然与NS记录冲突,因此你不能直接在自己购买的域名上为根@设置CNAME解析。
小芝士:最长匹配原则
我们都知道,cn.和com.cn.等许多二级ccTLD都归CNNIC管理,并且共用权威名称服务器。
那么,如果我们向CNNIC的某个权威NS(比如a.dns.cn)查询markdown.com.cn.的记录,服务器会直接返回markdown.com.cn.的NS,还是经历com.cn. → markdown.com.cn.两跳?
实际答案是前者:
查询 198.41.0.4
cn NS a.dns.cn
cn NS b.dns.cn
cn NS c.dns.cn
cn NS d.dns.cn
cn NS e.dns.cn
cn NS ns.cernet.net
a.dns.cn A 203.119.25.1
b.dns.cn A 203.119.26.1
c.dns.cn A 203.119.27.1
d.dns.cn A 203.119.28.1
e.dns.cn A 203.119.29.1
查询 a.dns.cn (203.119.25.1)
markdown.com.cn NS sixty.dnspod.net
markdown.com.cn NS medicine.dnspod.net
这是因为RFC 1034明确了最特定区域匹配的原则(或者理解为最长匹配原则)。CNNIC的权威NS同时管理着com.cn.zone和cn.zone两个区域文件,对于markdown.com.cn.而言前者显然完全匹配且更加具体。
[!note]
思考题:假如权威NS没有按照最特定区域匹配的原则进行响应,会出现什么问题?
要注意的是,edu.cn.的权威NS与cn.不同:
查询 198.41.0.4
cn NS a.dns.cn
cn NS b.dns.cn
cn NS c.dns.cn
cn NS d.dns.cn
cn NS e.dns.cn
cn NS ns.cernet.net
a.dns.cn A 203.119.25.1
b.dns.cn A 203.119.26.1
c.dns.cn A 203.119.27.1
d.dns.cn A 203.119.28.1
e.dns.cn A 203.119.29.1
查询 a.dns.cn (203.119.25.1)
edu.cn NS dns.edu.cn
edu.cn NS ns2.cernet.net
edu.cn NS ns4.cernet.net
edu.cn NS ns5.cernet.net
edu.cn NS dns2.edu.cn
edu.cn NS dns3.edu.cn
dns.edu.cn A 202.38.109.35
dns.edu.cn TYPE28 20010250c00600000000000000000035
dns2.edu.cn A 202.112.0.13
dns2.edu.cn TYPE28 20010da8000101000000000000000013
dns3.edu.cn A 101.4.62.35
Zone: edu.cn
查询 dns.edu.cn (202.38.109.35)
sysu.edu.cn NS ns3.sysu.edu.cn
sysu.edu.cn NS ns2.sysu.edu.cn
sysu.edu.cn NS ns1.sysu.edu.cn
ns3.sysu.edu.cn A 202.116.64.6
ns2.sysu.edu.cn A 202.116.64.2
ns1.sysu.edu.cn A 202.116.64.1
ns3.sysu.edu.cn TYPE28 20010250300200100000000000000006
ns2.sysu.edu.cn TYPE28 20010250300200100000000000000002
Zone: sysu.edu.cn
查询 ns3.sysu.edu.cn (202.116.64.6)
ns1.sysu.edu.cn A 202.116.64.1
ns2.sysu.edu.cn A 202.116.64.2
ns3.sysu.edu.cn A 202.116.64.6
ns1.sysu.edu.cn TYPE28 20010250300200100000000000000001
ns2.sysu.edu.cn TYPE28 20010250300200100000000000000002
附录
上述查询记录的源代码(Python原生):
希望它可以作为你亲自动手的开始
import socket
import struct
import random
from typing import List, Tuple, Optional, Dict
class DNSQuery:
"""DNS查询工具类"""
# 根服务器列表(使用部分根服务器)
ROOT_SERVERS = [
"198.41.0.4", # a.root-servers.net
"199.9.14.201", # b.root-servers.net
"192.33.4.12", # c.root-servers.net
"199.7.91.13", # d.root-servers.net
]
def __init__(self):
self.query_log = []
def build_dns_query(self, domain: str, qtype: int = 2) -> bytes:
"""
构建DNS查询包
:param domain: 要查询的域名
:param qtype: 查询类型 (1=A, 2=NS, 15=MX, 16=TXT)
:return: DNS查询包的字节数组
"""
# 生成随机事务ID
transaction_id = random.randint(0, 65535)
# DNS Header (12字节)
# ID (2字节) + Flags (2字节) + 4个计数字段 (各2字节)
flags = 0x0100 # 标准查询,期望递归
header = struct.pack('>HHHHHH',
transaction_id, # ID
flags, # Flags
1, # QDCOUNT (问题数)
0, # ANCOUNT (回答数)
0, # NSCOUNT (授权记录数)
0) # ARCOUNT (附加记录数)
# Question Section
question = self._encode_domain(domain)
question += struct.pack('>HH', qtype, 1) # QTYPE, QCLASS (1=IN)
return header + question
def _encode_domain(self, domain: str) -> bytes:
"""将域名编码为DNS格式"""
encoded = b''
for label in domain.split('.'):
length = len(label)
encoded += bytes([length]) + label.encode('ascii')
encoded += b'\x00' # 以0结尾
return encoded
def parse_dns_response(self, response: bytes) -> Dict:
"""
解析DNS响应包
:param response: DNS响应包
:return: 解析后的字典
"""
# 解析Header
header = struct.unpack('>HHHHHH', response[:12])
transaction_id, flags, qdcount, ancount, nscount, arcount = header
result = {
'transaction_id': transaction_id,
'flags': flags,
'questions': qdcount,
'answers': ancount,
'authority': nscount,
'additional': arcount,
'answer_records': [],
'authority_records': [],
'additional_records': []
}
# 跳过Question Section
offset = 12
for _ in range(qdcount):
offset, _ = self._skip_name(response, offset)
offset += 4 # QTYPE + QCLASS
# 解析Answer Section
for _ in range(ancount):
offset, record = self._parse_record(response, offset)
result['answer_records'].append(record)
# 解析Authority Section (NS记录通常在这里)
for _ in range(nscount):
offset, record = self._parse_record(response, offset)
result['authority_records'].append(record)
# 解析Additional Section (可能包含NS的A记录)
for _ in range(arcount):
offset, record = self._parse_record(response, offset)
result['additional_records'].append(record)
return result
def _skip_name(self, data: bytes, offset: int) -> Tuple[int, str]:
"""跳过并解析域名,返回新的offset和域名"""
labels = []
jumped = False
jump_offset = offset
while True:
if offset >= len(data):
break
length = data[offset]
if length == 0:
offset += 1
break
elif (length & 0xC0) == 0xC0: # 指针压缩
if not jumped:
jump_offset = offset + 2
jumped = True
pointer = struct.unpack('>H', data[offset:offset+2])[0]
offset = pointer & 0x3FFF
continue
else:
offset += 1
labels.append(data[offset:offset+length].decode('ascii', errors='ignore'))
offset += length
if jumped:
return jump_offset, '.'.join(labels)
return offset, '.'.join(labels)
def _parse_record(self, data: bytes, offset: int) -> Tuple[int, Dict]:
"""解析单条DNS记录"""
offset, name = self._skip_name(data, offset)
if offset + 10 > len(data):
return offset, {}
rtype, rclass, ttl, rdlength = struct.unpack('>HHIH', data[offset:offset+10])
offset += 10
record = {
'name': name,
'type': rtype,
'class': rclass,
'ttl': ttl,
'data': None
}
if offset + rdlength > len(data):
return offset, record
# 解析RDATA
if rtype == 1: # A记录
if rdlength == 4:
record['data'] = '.'.join(str(b) for b in data[offset:offset+4])
elif rtype == 2: # NS记录
_, ns_name = self._skip_name(data, offset)
record['data'] = ns_name
elif rtype == 5: # CNAME记录
_, cname = self._skip_name(data, offset)
record['data'] = cname
elif rtype == 6: # SOA记录
record['data'] = 'SOA'
else:
record['data'] = data[offset:offset+rdlength].hex()
offset += rdlength
return offset, record
def query_dns(self, server: str, domain: str, qtype: int = 2, port: int = 53) -> Optional[Dict]:
"""
向指定DNS服务器发送查询
:param server: DNS服务器IP
:param domain: 要查询的域名
:param qtype: 查询类型
:param port: 端口号
:return: 解析后的响应
"""
try:
query = self.build_dns_query(domain, qtype)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(5)
sock.sendto(query, (server, port))
response, _ = sock.recvfrom(4096)
sock.close()
return self.parse_dns_response(response)
except Exception as e:
return None
def recursive_query(self, domain: str):
"""
递归查询域名,从根服务器开始
:param domain: 要查询的域名
"""
print(f"\n查询 {self.ROOT_SERVERS[0]}")
response = self.query_dns(self.ROOT_SERVERS[0], domain, qtype=2)
if not response:
return
self._print_response(response)
# 从Authority Section获取下一级NS服务器
ns_servers = self._extract_ns_servers(response)
if not ns_servers:
return
# 第二步及后续:向TLD服务器查询
current_server = ns_servers[0]
while True:
print(f"\n查询 {current_server['ns']} ({current_server['ip']})")
response = self.query_dns(current_server['ip'], domain, qtype=2)
if not response:
break
self._print_response(response)
# 检查是否有Answer Section (表示找到了最终答案)
if response['answers'] > 0:
break
# 否则继续从Authority Section获取下一级服务器
ns_servers = self._extract_ns_servers(response)
if not ns_servers:
break
# 检查返回的zone是什么
if response['authority_records']:
zone = response['authority_records'][0].get('name', '')
print(f"Zone: {zone}")
current_server = ns_servers[0]
def _print_response(self, response: Dict):
"""打印DNS响应的详细信息"""
for record in response['authority_records']:
self._print_record(record)
for record in response['additional_records'][:5]:
self._print_record(record)
def _print_record(self, record: Dict):
"""打印单条记录"""
type_map = {1: 'A', 2: 'NS', 5: 'CNAME', 6: 'SOA'}
type_str = type_map.get(record.get('type'), f"TYPE{record.get('type')}")
print(f"{record.get('name'):25s} {type_str:5s} {record.get('data')}")
def _extract_ns_servers(self, response: Dict) -> List[Dict]:
"""从响应中提取NS服务器及其IP地址"""
ns_servers = []
# 从Authority Section获取NS记录
ns_names = []
for record in response['authority_records']:
if record.get('type') == 2: # NS记录
ns_names.append(record.get('data'))
# 从Additional Section获取对应的A记录
for ns_name in ns_names:
for record in response['additional_records']:
if record.get('type') == 1 and record.get('name') == ns_name:
ns_servers.append({
'ns': ns_name,
'ip': record.get('data')
})
break
return ns_servers
def main():
"""主函数"""
import sys
dns = DNSQuery()
# 如果有命令行参数,使用参数作为域名
# python3 main.py markdown.com.cn
if len(sys.argv) > 1:
domain = sys.argv[1]
else:
domain = "sysu.edu.cn"
dns.recursive_query(domain)
if __name__ == "__main__":
main()