欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 建筑 > 如何实现一个DNS

如何实现一个DNS

2025/3/20 14:57:32 来源:https://blog.csdn.net/General_zy/article/details/146300151  浏览:    关键词:如何实现一个DNS

目录

  • DNS简介
    • 记录类型
    • 传输协议
    • 域名空间(Domain Name Space)
      • 根域名服务器
    • 域名解析
      • DNS劫持
        • 公共DNS服务器(114DNS 、阿里DNS、百度DNS 、360 DNS、Google DNS)
  • 实现自己的DNS
  • go语言中强大的DNS库
    • 修改或伪造DNS 响应
  • CoreDNS
    • 安装
    • 插件
      • whoami
      • k8s
  • 附录
    • IDNA编码过程

DNS简介

在这里插入图片描述

域名系统(英语:Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCP和UDP端口53。当前,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。

开始时,域名的字符仅限于ASCII字符的一个子集。2008年,ICANN通过一项决议,允许使用其它语言作为互联网顶级域名的字符。使用基于Punycode码的IDNA系统,可以将Unicode字符串映射为有效的DNS字符集。因此,诸如“XXX.中国”、“XXX.台湾”的域名可以在地址栏直接输入并访问,而不需要安装插件。但是,由于英语的广泛使用,使用其他语言字符作为域名会产生多种问题,例如难以输入、难以在国际推广等。

国际化域名编码(英语:Punycode)是一种表示统一码和ASCII码的有限的字符集。例如中文“上海”会被编码为“fhqz97e”。国际化域名编码的目的是在于国际化域名标签(IDNA)的框架中,使这些(多语言)的域名可以编码为ASCII。编码语法在文档 RFC 3492 中规定。

DNS最早于1983年由保罗·莫卡派乔斯(Paul Mockapetris)发明;原始的技术规范在882号因特网标准草案(RFC 882)中发布。1987年发布的第1034和1035号草案修正了DNS技术规范,并废除了之前的第882和883号草案。在此之后对因特网标准草案的修改基本上没有涉及到DNS技术规范部分的改动。

早期的域名必须以英文句号.结尾。例如,当用户访问维基百科的HTTP服务时必须在地址栏中输入:http://www.wikipedia.org.,这样DNS才能够进行域名解析。如今DNS服务器已经可以自动补上结尾的句号。

记录类型

DNS系统中,常见的资源记录类型有:

  • 主机记录(A记录):RFC 1035定义,A记录是用于名称解析的重要记录,它将特定的主机名映射到对应主机的IP地址上。
  • 别名记录(CNAME记录): RFC 1035定义,CNAME记录用于将某个别名指向到某个A记录上,这样就不需要再为某个新名字另外创建一条新的A记录。
  • IPv6主机记录(AAAA记录): RFC 3596定义,与A记录对应,用于将特定的主机名映射到一个主机的IPv6地址。
  • 服务位置记录(SRV记录): RFC 2782定义,用于定义提供特定服务的服务器的位置,如主机(hostname),端口(port number)等。
  • 域名服务器记录(NS记录) :用来指定该域名由哪个DNS服务器来进行解析。 您注册域名时,总有默认的DNS服务器,每个注册的域名都是由一个DNS域名服务器来进行解析的,DNS服务器NS记录地址一般以以下的形式出现: ns1.domain.com、ns2.domain.com等。 简单的说,NS记录是指定由哪个DNS服务器解析你的域名。
  • NAPTR记录:RFC 3403定义,它提供了正则表达式方式去映射一个域名。NAPTR记录非常著名的一个应用是用于ENUM查询。

传输协议

DNS over UDP/TCP/53(Do53)
自 1983 年起,DNS 主要通过 UDP 端口 53 进行查询。此类查询由客户端以单个 UDP 数据包发送明文请求,服务器则以单个 UDP 数据包返回明文响应。当应答长度超过 512 字节,并且客户端与服务器都支持 DNS 扩展机制(EDNS)时,可能会使用更大的 UDP 数据包。

然而,DNS-over-UDP 存在诸多限制,包括缺乏传输层加密、身份验证、可靠传输和消息长度的限制。1989 年,RFC 1123 规定可选的 TCP 传输方式,用于 DNS 查询、响应,特别是区域传输。借助 TCP 连接的长响应分段机制,DNS 可支持更长的响应、更可靠的传输,并能复用客户端与服务器之间的长期连接。


DNS over TLS(DoT)
🔗 主条目:DNS over TLS

IETF 于 2016 年推出了加密 DNS 标准,即 DoT(DNS over TLS),利用传输层安全(TLS)协议保护整个 DNS 连接,而不仅仅是 DNS 负载数据。DoT 服务器监听 TCP 端口 853。根据 RFC 7858,DoT 既可以支持机会加密,也可以支持认证加密,但不会强制要求服务器或客户端认证。


DNS over QUIC(DoQ)
🔗 参考标准:RFC 9250

IETF 在 RFC 9250 中定义了 DNS over QUIC(DoQ),基于 QUIC 协议提供 DNS 解析服务,旨在提高传输效率并增强隐私保护。


DNS over HTTPS(DoH)
🔗 主条目:DNS over HTTPS

2018 年引入的 DoH(DNS over HTTPS)通过 HTTPS 隧道传输 DNS 查询数据,从而借助 TLS 保护 DNS 解析过程。由于 DoH 使用 TCP 端口 443,与普通 HTTPS 流量相同,因此它比传统 DNS 更难以被拦截或审查。

DoH 常被推广为“对网络更友好的 DNS 方案”,因为它在 HTTPS 流量中隐藏 DNS 请求,使其难以被检测。然而,DoH 也因降低用户匿名性而饱受批评,因为与 DoT 相比,DoH 的查询流量模式更容易暴露用户行为。


Oblivious DNS(ODNS)与 Oblivious DoH(ODoH)
🔗 相关研究:ODNS 论文

Oblivious DNS(ODNS)由普林斯顿大学和芝加哥大学的研究人员发明,旨在增强未加密 DNS 的隐私保护,早于 DoH 标准化和广泛部署。

后来,Apple 和 Cloudflare 在 DoH 环境中部署了类似技术,称为 Oblivious DoH(ODoH)。ODoH 结合了 ODNS 的入口/出口分离机制与 DoH 的 HTTPS 隧道和 TLS 加密,实现更强的隐私保护。


DNS over Tor
DNS 可以通过 VPN 和隧道进行传输,DNS-over-Tor 作为一种隐私保护方案,自 2019 年起已广泛应用。通过 Tor 网络的入口和出口节点进行 DNS 查询,同时结合 TLS 加密,能够有效防止流量分析,提供更高的匿名性。


DNSCrypt
🔗 主条目:DNSCrypt

DNSCrypt 于 2011 年由 OpenDNS 在 IETF 标准框架之外开发,其主要特点是:

  • 在递归解析器的下游侧引入 DNS 加密
  • 客户端使用服务器的公钥对查询进行加密,并依赖 DNSSEC 进行公钥验证
  • 使用 TCP 或 UDP 端口 443,与 HTTPS 流量共享端口,增强防火墙穿越能力

2019 年,DNSCrypt 进一步扩展,支持 匿名模式,类似于 Oblivious DNS。该模式下,入口节点接收使用不同服务器公钥加密的查询并转发至目标服务器,该服务器作为出口节点执行递归解析:

  • 入口节点 知道客户端身份,但不知道查询内容
  • 出口节点 知道查询内容,但不知道客户端身份

DNSCrypt 已被多个免费和开源软件实现,并额外集成了 ODoH,支持多种操作系统,包括 Unix、iOS、Linux、Android 和 Windows。


方案端口加密方式主要特点
Do5353 (UDP/TCP)传统 DNS,明文传输,安全性较低
DoT853 (TCP)TLS端到端加密,适用于 ISP 级别
DoH443 (TCP)HTTPS (TLS)通过 HTTPS 隧道传输,难以拦截,但降低匿名性
DoQ853 or 443 (QUIC)QUIC (TLS)低延迟,UDP 传输,减少握手开销
ODNS / ODoH443 (HTTPS)TLS + 入口/出口分离进一步增强匿名性
DNSCrypt443 (TCP/UDP)公钥加密端对端加密,支持匿名模式

不同的 DNS 传输协议各有优缺点,选择哪种方案取决于对隐私保护、性能和兼容性的需求。

域名空间(Domain Name Space)

域名空间由树形数据结构组成。树中的每个节点或叶子都有一个标签(label),并且可能包含零个或多个资源记录(RR,Resource Record),这些记录存储了与该域名相关的信息。域名本身由标签与其父节点的名称拼接而成,二者之间用点(.)分隔。

整棵树从根区域(Root Zone)开始划分成多个区域(Zone)。一个DNS区域可以包含任意多个域名和子域名,由区域管理员自由决定。DNS还可以按类别(Class)进行分区,每个类别可以看作是一组并行的命名空间树。

在互联网类别(Class Internet)下,域名系统采用分层结构,每个区域由一个名称服务器(Name Server)提供服务。

域名系统是一个分布式数据库,采用客户端-服务器(Client-Server) 模型。数据库的节点就是名称服务器(Name Server)。每个域至少有一个权威DNS服务器(Authoritative DNS Server),负责公布该域的信息以及其子域的名称服务器。整个系统的顶层由根名称服务器(Root Name Servers) 维护,处理顶级域(TLDs) 的查询。

权威名称服务器 仅从原始数据源(例如域管理员或动态DNS机制)获取数据并直接响应DNS查询,而不会从其他服务器缓存数据。

权威名称服务器可以分为:

  • 主服务器(Primary Server):存储所有区域记录的原始副本。
  • 从服务器(Secondary Server):通过DNS协议的自动更新机制,与主服务器同步以保持副本一致。

每个DNS区域必须配置一组权威名称服务器,这些服务器的信息存储在上级区域的NS(Name Server)记录中。

权威服务器在响应中设置AA(Authoritative Answer)标志,表示其响应具有权威性。在 dig 等DNS管理工具的查询结果中,该标志通常会被突出显示。

如果一个名称服务器被指定为某个域的权威服务器,但实际上并没有相关数据,则会返回错误(Lame Delegation / Lame Response)。

对于某个区域的管理权限可以通过创建新的区域进行划分。新区域的权限被委派(Delegation)给指定的名称服务器,原父区域不再对新区域拥有权威性。

在这里插入图片描述

根域名服务器

根域名服务器(英语:root name server,简称“根域名服务器”)是互联网域名解析系统(DNS)中最高级别的域名服务器,负责返回顶级域的权威域名服务器地址。它们是互联网基础设施中的重要部分,因为所有域名解析操作均离不开它们。由于DNS和某些协议(未分片的用户数据报协议(UDP)数据包在IPv4内的最大有效大小为512字节)的共同限制,根域名服务器地址的数量被限制为13个。幸运的是,采用任播技术架设镜像服务器可解决该问题,并使得实际运行的根域名服务器数量大大增加。截至2023年6月,全球共有1719台根域名服务器在运行。

任播(英语:anycast)是一种网络寻址和路由的策略,使得资料可以根据路由拓扑来决定送到“最近”或“最好”的目的地。

任播是与单播(unicast)、广播(broadcast)和多播(multicast)不同的方式。

  • 在单播中,在网络地址和网络节点之间存在一一对应的关系。
  • 在广播和多播中,在网络地址和网络节点之间存在一对多的关系:每一个发送地址对应一群接收可以复制信息的节点。
  • 在任播中,在网络地址和网络节点之间存在一对多的关系:每一个地址对应一群接收节点,但在任何给定时间,- 只有其中之一可以接收到发送端来的信息。

在互联网中,通常使用边界网关协议(BGP)来实现任播。

全球13组根域名服务器以英文字母A到M依序命名,域名格式为“字母.root-servers.net”。全部已利用任播技术在全球多个地点设立镜像站。

字母 IPv4地址 IPv6地址 自治系统编号[4] 曾用名 运营单位 设置地点
#数量(全球/地区)[5]
软件
A 198.41.0.4 2001:503:ba3e::2:30 AS26415, AS19836,[4][注 1] AS36619, AS36620, AS36622, AS36625, AS36631, AS64820[注 2][6] ns.internic.net 威瑞信 以任播技术设置于多处
5/0
NSD、威瑞信ATLAS
B 199.9.14.201[注 3][7] [8] 2001:500:200::b[9] AS394353[10] ns1.isi.edu 美国南加州大学信息学研究所 以任播技术设置于多处
2/0
BIND
C 192.33.4.12 2001:500:2::c AS2149[4][11] c.psi.net Cogent通信 以任播技术设置于多处
8/0
BIND
D 199.7.91.13[注 4][12] 2001:500:2d::d AS10886[4][13] terp.umd.edu 美国马里兰大学学院市分校 以任播技术设置于多处
50/67
BIND
E 192.203.230.10 2001:500:a8::e AS21556[4][14] ns.nasa.gov 美国国家航空航天局埃姆斯研究中心英语NASA Ames Research Center 以任播技术设置于多处
125/141
BIND、NSD
F 192.5.5.241 2001:500:2f::f AS3557,[4][15] AS1280, AS30132[15] ns.isc.org 互联网系统协会英语Internet Systems Consortium 以任播技术设置于多处
57/0
BIND [16]
G[注 5] 192.112.36.4[注 6] 2001:500:12::d0d[注 7] AS5927[4][17] ns.nic.ddn.mil 美国国防信息系统局英语Defense Information Systems Agency 以任播技术设置于多处
6/0
BIND
H 198.97.190.53[注 8][18] 2001:500:1::53[注 9][19] AS1508[19][注 10][20] aos.arl.army.mil 美国陆军研发实验室英语United States Army Research Laboratory 美国马里兰州阿伯丁试验场
以及加利福尼亚州圣地亚哥
2/0
NSD
I 192.36.148.17 2001:7fe::53 AS29216[4][21] nic.nordu.net Netnod英语Netnod Internet Exchange i Sverige 以任播技术设置于多处
58/0
BIND
J 192.58.128.30[注 11] 2001:503:c27::2:30 AS26415,[4][22] AS36626, AS36628, AS36632[22] 不适用 威瑞信 以任播技术设置于多处
61/13
NSD、威瑞信ATLAS
K 193.0.14.129 2001:7fd::1 AS25152[4][23][24] 不适用 欧洲IP资源网络协调中心英语RIPE NCC 以任播技术设置于多处
5/23
BIND、NSDKnot DNS英语Knot DNS[25]
L 199.7.83.42[注 12][26] 2001:500:9f::42[注 13][27] AS20144[4][28][29] 不适用 ICANN 以任播技术设置于多处
161/0
NSDKnot DNS英语Knot DNS[30]
M 202.12.27.33 2001:dc3::35 AS7500[4][31][32] 不适用 日本WIDE项目英语WIDE Project 以任播技术设置于多处
6/1
BIND

域名解析

zh.wikipedia.org 作为一个域名就和IP地址198.35.26.96 相对应。DNS就像是一个自动的电话号码簿,我们可以直接拨打198.35.26.96 的名字zh.wikipedia.org 来代替电话号码(IP地址)。DNS在我们直接调用网站的名字以后就会将像zh.wikipedia.org 一样便于人类使用的名字转化成像198.35.26.96 一样便于机器识别的IP地址。

DNS查询有两种方式:递归和迭代DNS客户端设置使用的DNS服务器一般都是递归服务器,它负责全权处理客户端的DNS查询请求,直到返回最终结果。而DNS服务器之间一般采用迭代查询方式。

以查询 zh.wikipedia.org 为例:

  1. 客户端发送查询报文"query zh.wikipedia.org"至DNS服务器,DNS服务器首先检查自身缓存,如果存在记录则直接返回结果。
  2. 如果记录老化或不存在,则:
    1. DNS服务器向根域名服务器发送查询报文"query zh.wikipedia.org",根域名服务器返回顶级域 .org 的顶级域名服务器地址。
    2. DNS服务器向 .org 域的顶级域名服务器发送查询报文"query zh.wikipedia.org",得到二级域 .wikipedia.org 的权威域名服务器地址。
    3. DNS服务器向 .wikipedia.org 域的权威域名服务器发送查询报文"query zh.wikipedia.org",得到主机 zh 的A记录,存入自身缓存并返回给客户端。

一个域名的所有者可以通过查询WHOIS数据库而被找到;对于大多数根域名服务器,基本的WHOIS由ICANN维护,而WHOIS的细节则由控制那个域的域注册机构维护。

对于240多个国家代码顶级域名(ccTLDs),通常由该域名权威注册机构负责维护WHOIS。例如中国互联网络信息中心(China Internet Network Information Center)负责.CN域名的WHOIS维护,香港互联网注册管理有限公司(Hong Kong Internet Registration Corporation Limited)负责.HK域名的WHOIS维护,台湾网络信息中心(Taiwan Network Information Center)负责.TW域名的WHOIS维护。

在这里插入图片描述

DNS劫持

DNS(域名系统)劫持又叫域名劫持,指攻击者利用其他攻击手段,篡改了某个域名的解析结果,使得指向该域名的IP变成了另一个IP,导致对相应网址的访问被劫持到另一个不可达的或者假冒的网址,从而实现非法窃取用户信息或者破坏正常网络服务的目的。

据本人所知,某国家多个地区大范围常年出现DNS劫持,真的猜不透。

在这里插入图片描述
由于域名劫持往往只能在特定的被劫持的网络范围内进行,所以在此范围外的域名服务器(DNS)能够返回正常的IP地址,**高级用户可以在网络设置把DNS指向这些正常的域名服务器以实现对网址的正常访问。**所以域名劫持通常相伴的措施——封锁正常DNS的IP。如果知道该域名的真实IP地址,则可以直接用此IP代替域名后进行访问。比如访问百度域名,可以把访问改为202.108.22.5,从而绕开域名劫持 。

DNS劫持的应对方法:

DNS劫持十分凶猛且不容易被用户感知,攻击者利用宽带路由器的缺陷对用户DNS进行篡改——用户只要浏览一下攻击者所掌控的WEB页面,其宽带路由器的DNS就会被篡改,因为该WEB页面设有特别的恶意代码,所以可以成功躲过检测,导致大量用户被DNS劫持。

怎么防止DNS劫持攻击?

1、建议使用复杂的密码重置路由器的默认密码。

2、使用DNS注册器时使用双因素身份验证,并修补路由器中存在的所有漏洞以避免危害。

3、最好远离不受信任的网站,避免下载任何免费的东西。

4、如果您已被感染,建议删除HOSTS文件的内容并重置Hosts File。

5、为防止DNS劫持,始终建议使用良好的安全软件和防病毒程序,并确保定期更新软件。

6、安全专家建议使用公共DNS服务器。

7、最好定期检查您的DNS设置是否已修改,并确保您的DNS服务器是安全的。

公共DNS服务器(114DNS 、阿里DNS、百度DNS 、360 DNS、Google DNS)

114 DNS:高速 电信联通移动全国通用DNS

纯净 无劫持 无需再忍受被强扭去看广告或粗俗网站之痛苦
服务ip为:114.114.114.114 和 114.114.115.115

拦截 钓鱼病毒木马网站 增强网银、证券、购物、游戏、隐私信息安全
服务ip为:114.114.114.119 和 114.114.115.119

学校或家长可选拦截 色情网站 保护少年儿童免受网络色情内容的毒害
服务ip为:114.114.114.110 和 114.114.115.110

阿里DNS

服务ip为:223.5.5.5和223.6.6.6 阿里巴巴集团众多优秀工程师开发维护的公共DNS—AliDNS

2019年支持ipv6dns了,IPv6:2400:3200::1和2400:3200:baba::1

百度DNS

服务IP为:180.76.76.76 百度公共DNS是百度系统部推出的递归DNS解析服务。

Google DNS

8.8.8.8和8.8.4.4

实现自己的DNS

DNS 使用二进制格式进行数据传输,基本格式如下:

在这里插入图片描述
这个报文由 12 bytes 的首部和 4 个长度可变的字段组成。

Header(12 字节)

字段长度说明
ID2B标识查询和响应
Flags2B标志位,包含是否是查询/响应、查询类型等
QDCOUNT2B问题数目
ANCOUNT2B答案数目
NSCOUNT2B权威记录数目
ARCOUNT2B附加记录数目
  • 标识:由 client 设置并由 server 返回结果,用以确定响应与查询是否匹配;
  • 标志:16 位的标志字段被划分为若干子字段,如下图所示:
    在这里插入图片描述
  • 问题数、资源记录数、授权资源记录数和额外资源记录数分别对应最后 4 个可变长字段中包含的条目数,对于查询报文,问题数通常是 1,而其他 3 项均为 0;对于应答报文,回答数至少为 1,剩下的两项可以是 0 或非 0;

Question

字段说明
QNAME查询的域名(以 . 结束,每个部分前有长度)
QTYPE2B,查询类型(A=1, AAAA=28, NS=2, CNAME=5等)
QCLASS2B,一般为 1(表示 IN 互联网)

问题部分中每个问题的格式如下所示(通常只有 1 个问题):

在这里插入图片描述
查询名:即要查找的 DNS 名字,本质上一个分段带长度的字符序列,每个标识符以首字节的计数值来说明随后标识符的字节长度,每个名字以字节 0 结束。长度为 0 的标识符是根标识符。

计数字节的值必须是 0~63 的数,因此标识符的最大长度仅为 63。

该字段无须对齐和填充,如下是域名 gemini.tuc.noao.edu. 的表示:

在这里插入图片描述
查询类型:每个问题都有一个查询类型,而每个响应(即每个资源记录)也有一个类型。比如 A 记录的话,该字段为 1;

查询类:通常是 1,指互联网地址(IP);

Answer

字段说明
NAME2B,响应的域名
TYPE2B,响应类型
CLASS2B,通常是 1(IN)
TTL4B,缓存时间
RDLENGTH2B,资源记录长度
RDATA变长,实际 IP 地址等信息

DNS 报文中最后三个字段均采用资源记录 RR(Resource Record)的相同格式,如下所示:
在这里插入图片描述

  • 域名:记录中数据对应的名字,格式与前文查询报文中对应字段一致;

  • 类型:与前文中的查询报文中的查询类型一致;

  • 类:通常是 1,指互联网数据;

  • 生存时间:客户端程序保留该资源记录的秒数,通常为 2 天;

  • 资源记录长度:说明资源记录的数量,该数据的格式依赖于类型字段的值。对于类型 1(A 记录)的资源数据是 4 字节的 IP 地址;

go语言中强大的DNS库

github.com/miekg/dns 是一个用于 Go 语言的 DNS 库,它提供了丰富的功能来处理 DNS 查询、响应、解析和构建 DNS 消息。

这个库非常灵活和强大,被广泛用于构建 DNS 客户端和服务器应用程序。

主要功能:

  1. DNS 查询与响应:
    • 支持各种类型的 DNS 查询(如 A、AAAA、MX、NS、TXT 等)。
    • 支持发送和接收 DNS 消息。
  2. DNS 消息构建与解析:
    • 提供便捷的方法来构建 DNS 消息(如请求和响应)。
    • 能够解析 DNS 消息并提取所需信息。
  3. DNS 服务器:
    • 可以用来构建自定义的 DNS 服务器。
    • 支持处理多种类型的 DNS 请求。
  4. 扩展性:
    • 支持扩展和自定义,允许用户添加自己的功能和处理逻辑。

安装:go get github.com/miekg/dns

github.com/miekg/dns 是一个基于 Go 的 DNS 库,支持 DNS 的所有特性,严格遵循 Less is more 的原则,被广泛用在很多著名的开源项目中,比如 CoreDNS(作者同时也是 CoreDNS 的核心开发者)。

主要功能:

  1. DNS 查询与响应:
    • 支持各种类型的 DNS 查询(如 A、AAAA、MX、NS、TXT 等)。
    • 支持发送和接收 DNS 消息。
  2. DNS 消息构建与解析:
    • 提供便捷的方法来构建 DNS 消息(如请求和响应)。
    • 能够解析 DNS 消息并提取所需信息。
  3. DNS 服务器:
    • 可以用来构建自定义的 DNS 服务器。
    • 支持处理多种类型的 DNS 请求。
  4. 扩展性:
    • 支持扩展和自定义,允许用户添加自己的功能和处理逻辑。

来看一个实际案例:

package mainimport ("fmt""log""net""github.com/miekg/dns"
)func main() {// 注册 DNS 请求处理函数dns.HandleFunc(".", handleDNSRequest)// 设置服务器地址和协议server := &dns.Server{Addr: ":53", Net: "udp"}log.Printf("Starting DNS server on %s\n", server.Addr)if err := server.ListenAndServe(); err != nil {log.Fatalf("Failed to start DNS server: %v\n", err)}
}// 处理 DNS 请求的函数
func handleDNSRequest(w dns.ResponseWriter, requestMsg *dns.Msg) {msg := new(dns.Msg)msg.SetReply(requestMsg)// 将 DNS 响应标记为权威应答msg.Authoritative = true// 将 DNS 响应标记为递归可用// msg.RecursionAvailable = true// 遍历请求中的问题部分,生成相应的回答for _, question := range requestMsg.Question {fmt.Println("请求解析的域名:", question.Name)switch question.Qtype {case dns.TypeA:handleARecord(question, msg)case dns.TypeAAAA:handleAAAARecord(question, msg)// 可以在这里添加其他类型的记录处理逻辑}}w.WriteMsg(msg)
}// 构建 A 记录的函数
func handleARecord(q dns.Question, msg *dns.Msg) {ip := net.ParseIP("192.0.2.1")rr := &dns.A{Hdr: dns.RR_Header{Name:   q.Name,Rrtype: dns.TypeA,Class:  dns.ClassINET,Ttl:    600,},A: ip,}msg.Answer = append(msg.Answer, rr)
}func handleAAAARecord(q dns.Question, msg *dns.Msg) {ip := net.ParseIP("240c::6666")rr := &dns.AAAA{Hdr: dns.RR_Header{Name:   q.Name,Rrtype: dns.TypeAAAA,Class:  dns.ClassINET,Ttl:    600,},AAAA: ip,}msg.Answer = append(msg.Answer, rr)
}

解释:

  1. dns.HandleFunc(".", handleDNSRequest):注册一个处理函数 handleDNSRequest,它将处理所有的 DNS 请求(“.” 表示所有域名)。

  2. server := &dns.Server{Addr: ":53", Net: "udp"}:创建一个 DNS 服务器,监听 :53 端口并使用 UDP 协议。

  3. 处理 DNS 请求的函数:handleDNSRequest函数处理传入的 DNS 请求。它会根据请求中的问题(r.Question)生成响应消息,并将其发送回客户端。

  4. 构建 A 记录的函数:handleARecord生成一个 A 记录的响应,其中 IP 地址为 192.0.2.1,并将其添加到响应消息中。

  5. 处理其他类型的 DNS 记录:你可以扩展 handleDNSRequest 函数来处理其他类型的 DNS 记录,例如 CNAME、MX、TXT 等。只需在 switch 语句中添加相应的处理函数:

    switch question.Qtype {
    case dns.TypeA:handleARecord(question, msg)
    case dns.TypeCNAME:handleCNAMERecord(question, msg)
    case dns.TypeMX:handleMXRecord(question, msg)
    case dns.TypeTXT:handleTXTRecord(question, msg)
    }
    

通过这种方式,你可以构建一个功能全面的自定义 DNS 服务器,能够处理多种类型的 DNS 查询并返回相应的响应。

然后我们来写一个客户端用于测试:

package mainimport ("testing""github.com/miekg/dns"
)func TestDNSHandler(t *testing.T) {// 创建测试请求m := new(dns.Msg)m.SetQuestion("example.com.", dns.TypeA)// 发送请求到本地服务器c := new(dns.Client)in, _, err := c.Exchange(m, "127.0.0.1:53")if err != nil {t.Fatalf("DNS请求失败: %v", err)}if len(in.Answer) < 1 {t.Fatal("未收到DNS响应")}t.Logf("收到响应:%+v", in.Answer)
}

或者也可以用python写dns客户端:

pip install dnspythonimport dns.resolverdef test_dns_server(domain="example.com", dns_server="127.0.0.1", port=53):# 创建自定义解析器resolver = dns.resolver.Resolver()resolver.nameservers = [dns_server]resolver.port = porttry:# 发起A记录查询answer = resolver.resolve(domain, "A")print(f"DNS服务器 {dns_server}:{port} 响应成功")print("响应结果:")for rr in answer:print(f"{domain} 的IP地址为 {rr.address}")except dns.exception.DNSException as e:print(f"DNS查询失败: {e}")if __name__ == "__main__":test_dns_server(domain="google.com")  # 测试域名test_dns_server(domain="nonexistent.abc")  # 测试不存在的域名

我们可以看到日志输出:
在这里插入图片描述
在这里插入图片描述
这样我们的DNS就算完成了。

修改或伪造DNS 响应

本地监听在53端口接收DNS请求,并将请求转发给上游DNS服务器。

当发现请求解析的域名是www.baidu.com时,会修改响应中的A和AAAA记录,将其替换为指定的IP地址。
然后返回给客户端。

package mainimport ("fmt""log""net""github.com/miekg/dns"
)func main() {// 注册 DNS 请求处理函数dns.HandleFunc(".", HandleDNSRequest)// 设置服务器地址和协议server := &dns.Server{Addr: ":53", Net: "udp"}log.Printf("Starting DNS server on %s\n", server.Addr)if err := server.ListenAndServe(); err != nil {log.Fatalf("Failed to start DNS server: %v\n", err)}
}func HandleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {c := new(dns.Client)c.Net = "udp"var upstreamDNS stringif w.RemoteAddr().(*net.UDPAddr).IP.To4() != nil {upstreamDNS = "192.168.140.3:53"} else {upstreamDNS = "[240c::6666]:53"}resp, _, err := c.Exchange(r, upstreamDNS)if err != nil {fmt.Printf("查询上游DNS时出错: %v\n", err)return}for _, question := range resp.Question {fmt.Println("查询的域名:", question.Name)if question.Name == "www.baidu.com." {// 修改响应中的 A 和 AAAA 记录for _, answer := range resp.Answer {switch rr := answer.(type) {case *dns.A:fmt.Printf("替换前的A类型地址: %s\n", rr.A.String())rr.A = net.ParseIP("1.2.3.4")fmt.Printf("替换后的A类型地址: %s\n", rr.A.String())case *dns.AAAA:fmt.Printf("替换前的AAAA类型地址: %s\n", rr.AAAA.String())rr.AAAA = net.ParseIP("::1")fmt.Printf("替换后的AAAA类型地址: %s\n", rr.AAAA.String())}}}}w.WriteMsg(resp)
}

当然,也可以完全自定义DNS记录

package mainimport ("fmt""log""net""strings""github.com/miekg/dns"
)func main() {// 注册 DNS 请求处理函数dns.HandleFunc(".", HandleDNSRequest)// 设置服务器地址和协议server := &dns.Server{Addr: ":53", Net: "udp"}log.Printf("Starting DNS server on %s\n", server.Addr)if err := server.ListenAndServe(); err != nil {log.Fatalf("Failed to start DNS server: %v\n", err)}
}func HandleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {c := new(dns.Client)c.Net = "udp"var upstreamDNS stringif w.RemoteAddr().(*net.UDPAddr).IP.To4() != nil {upstreamDNS = "192.168.140.3:53"} else {upstreamDNS = "[240c::6666]:53"}resp, _, err := c.Exchange(r, upstreamDNS)if err != nil {fmt.Printf("查询上游DNS时出错: %v\n", err)return}var (newARecords    = []string{} // 替换的 A 记录地址newAAAARecords = []string{} // 替换的 AAAA 记录地址)// 存储去重后的记录var newAnswers []dns.RRhost := strings.TrimSuffix(r.Question[0].Name, ".")for _, question := range resp.Question {fmt.Println("查询的域名:", question.Name)if host == "www.baidu.com" {A := falseAAAA := falsefor _, answer := range resp.Answer {switch rr := answer.(type) {case *dns.A:fmt.Println("解析的v4原地址:", rr.A.String())if A {continue}A = truecase *dns.AAAA:fmt.Println("解析的v6原地址:", rr.AAAA.String())if AAAA {continue}AAAA = true}}if A {newARecords = append(newARecords, "192.168.1.2")// 处理 A 记录for _, addr := range newARecords {newRR := &dns.A{Hdr: dns.RR_Header{Name:   r.Question[0].Name,Rrtype: dns.TypeA,Class:  dns.ClassINET,Ttl:    300,},A: net.ParseIP(addr),}newAnswers = append(newAnswers, newRR)}// 替换响应中的记录resp.Answer = newAnswers}if AAAA {newAAAARecords = append(newAAAARecords, "2001:1:2:3:4::5")// 处理 AAAA 记录for _, addr := range newAAAARecords {newRR := &dns.AAAA{Hdr: dns.RR_Header{Name:   r.Question[0].Name,Rrtype: dns.TypeAAAA,Class:  dns.ClassINET,Ttl:    300,},AAAA: net.ParseIP(addr),}newAnswers = append(newAnswers, newRR)}// 替换响应中的记录resp.Answer = newAnswers}}}w.WriteMsg(resp)
}

这里实现了一个 DNS 服务器,通过 HandleDNSRequest 函数处理传入的 DNS 请求,并根据请求的域名和类型替换响应中的记录。

如果查询的是 www.baidu.com,且存在 A 记录或 AAAA 记录,则将这些记录分别替换为伪造的 IP 地址 192.168.1.2 和 2001:1:2:3:4::5。

CoreDNS

在这里插入图片描述

CoreDNS是Golang编写的一个插件式DNS服务器,是Kubernetes 1.13 后所内置的默认DNS服务器。采用的开源协议为Apache License Version 2。

CoreDNS也是CNCF孵化项目,目前已经从CNCF毕业。CoreDNS 的目标是成为 Cloud Native(云原生)环境下的 DNS 服务器和服务发现解决方案。

CoreDNS 不同于其他 DNS 服务器,如(均为出色服务器)BIND、Knot、PowerDNS 和 Unbound(技术上是一个解析器,但值得一提),因为它非常灵活,且几乎所有功能都外包给插件。

插件可以独立存在或协同工作以执行“DNS 功能”。

目前默认 CoreDNS 安装包含大约 30 个插件,但还有一大批 外部 插件,可以将它们编译到 CoreDNS 中以扩展其功能。

安装

可以在github下载windows版的coreDNS:https://github.com/coredns/coredns/releases/tag/v1.12.0

不愧是go写的项目:

在这里插入图片描述
一旦有了coredns二进制文件,你可以使用-plugins标志列出所有已编译的插件。

在这里插入图片描述
如果没有Corefile,CoreDNS 将加载whoami插件,该插件将使用客户端的 IP 地址和端口号做出响应。

在这里插入图片描述

插件

一旦启动 CoreDNS 并解析了配置,它将运行服务器。每个服务器都通过其所服务的区域端口来定义,并且每个服务器都有自己的插件链

当 CoreDNS 处理查询时,将执行以下步骤:

  1. 选择最具体的服务器
    如果配置了多个监听查询端口的服务器,CoreDNS 将检查哪个服务器提供了最具体的区域(最长后缀匹配)。
    例如,假设有两个服务器:

    • 一个用于 example.org
    • 另一个用于 a.example.org

    当查询 www.a.example.org 时,它将被路由到第二个服务器(a.example.org)。

  2. 执行插件链
    找到合适的服务器后,查询将通过该服务器配置的插件链进行路由。这始终按相同顺序执行,该(静态)顺序定义在 plugin.cfg 中。

  3. 插件的处理逻辑
    每个插件都会检查查询,并决定是否要处理它,可能的情况如下:

    • 插件直接处理查询,返回响应。
    • 插件不处理查询,让下一个插件继续处理。
    • 插件部分处理查询,但仍需调用下一个插件(即贯通模式,通常由 fallthrough 关键词启用)。
    • 插件处理查询并添加“提示”,然后调用下一个插件。这个提示提供了一种查看最终响应并采取措施的方式。
  4. 查询响应
    处理查询意味着插件会向客户端返回最终的响应。虽然插件可以偏离上述逻辑,但 CoreDNS 现有的所有插件都属于这四类之一。

可以在 CoreDNS 中配置多个内容。首先需要确定要将哪些插件编译到 CoreDNS 中。官网提供的二进制文件具有 plugin.cfg 中列出的所有插件,已编译其中

各个服务器块以 Server 应管理的区域开始。在区域名称或区域名称列表(用空格分隔)之后,使用左大括号打开服务器块。使用右大括号关闭服务器块。以下服务器块指定了对根区域以下所有区域负责的服务器:.;从根本上说,此服务器应处理每一个可能出现的查询。

. {# Plugins defined here.
}

如,

coredns.io:5300 {file /etc/coredns/zones/coredns.io.db
}example.io:53 {errorslogfile /etc/coredns/zones/example.io.db
}example.net:53 {file /etc/coredns/zones/example.net.db
}.:53 {errorsloghealthrewrite name foo.example.com foo.default.svc.cluster.localkubernetes cluster.local 10.0.0.0/24file /etc/coredns/example.db example.orgforward . /etc/resolv.confcache 30
}

服务器块可以选择指定侦听的端口号。默认值为端口 53(DNS 的标准端口)。可以通过在冒号分隔的区域后列出端口来指定端口。此 Corefile 指示 CoreDNS 创建侦听端口 1053 的服务器

.:1053 {# Plugins defined here.
}

目前 CoreDNS 接受四种不同的协议:DNS、DNS over TLS (DoT)、DNS over HTTP/2 (DoH) 和 DNS over gRPC。您可以通过使用方案为区域名称打前缀来在服务器配置中指定服务器应接受的内容。

  • 用于普通 DNS 的dns://(如果未指定方案,则为默认值)。
  • 用于 DNS over TLS 的tls://,请参见 RFC 7858。
  • 用于 DNS over HTTPS 的https://,请参见 RFC 8484。
  • 用于 DNS over gRPC 的grpc://。

whoami

下载coreDNS的源码,我们可以在plugin目录下看到所有插件,并且plugin.go中定义了一个plugin的行为:
在这里插入图片描述
*dns.Msg就是github.com/miekg/dns中的DNS请求对象,我们进入whoami:

const name = "whoami"// Whoami is a plugin that returns your IP address, port and the protocol used for connecting
// to CoreDNS.
type Whoami struct{}// ServeDNS implements the plugin.Handler interface.
func (wh Whoami) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {state := request.Request{W: w, Req: r}a := new(dns.Msg)a.SetReply(r)a.Authoritative = trueip := state.IP()var rr dns.RRswitch state.Family() {case 1:rr = new(dns.A)rr.(*dns.A).Hdr = dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeA, Class: state.QClass()}rr.(*dns.A).A = net.ParseIP(ip).To4()case 2:rr = new(dns.AAAA)rr.(*dns.AAAA).Hdr = dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeAAAA, Class: state.QClass()}rr.(*dns.AAAA).AAAA = net.ParseIP(ip)}srv := new(dns.SRV)srv.Hdr = dns.RR_Header{Name: "_" + state.Proto() + "." + state.QName(), Rrtype: dns.TypeSRV, Class: state.QClass()}if state.QName() == "." {srv.Hdr.Name = "_" + state.Proto() + state.QName()}port, _ := strconv.ParseUint(state.Port(), 10, 16)srv.Port = uint16(port)srv.Target = "."a.Extra = []dns.RR{rr, srv}w.WriteMsg(a)return 0, nil
}// Name implements the Handler interface.
func (wh Whoami) Name() string { return name }

whoami 插件的功能是返回客户端的 IP 地址、端口号和使用的协议,相当于 DNS 版的 “what is my IP” 服务,因此实现非常简单。

k8s

在 k8s 集群中,CoreDNS 运行在 kube-system 命名空间,通常作为 Deployment 管理,并通过 kube-dns Service 暴露出来。

所有 Pod 解析域名时,都会请求 CoreDNS,而 CoreDNS 解析 Service 和 Pod 的 DNS 记录,提供 IP 地址。

DNS 解析流程:

  • Pod 内部的 /etc/resolv.conf 配置 nameserver 指向 CoreDNS。
  • 当 Pod 访问 service-name.namespace.svc.cluster.local 时,DNS 请求被发往 CoreDNS。
  • CoreDNS 查询 kube-apiserver,获取 Service 对应的 ClusterIP 并返回。

举两个例子,我有一个Web App,比如是python的flask服务配了mysql:

import pymysql
conn = pymysql.connect(host="mysql.db-namespace.svc.cluster.local", port=3306, user="root", password="password", database="mydb")

可以看到我此处填写的是svc的名称,连接建立时:

  • Pod 内部执行 nslookup mysql.db-namespace.svc.cluster.local,通过上文可以知道这个请求会发到coreDNS
  • CoreDNS 解析,返回 MySQL Service 的 ClusterIP

Web App 连接 ClusterIP: 10.96.0.20:3306

如果 MySQL 是 Headless Service(clusterIP: None),CoreDNS 会直接返回 MySQL Pod 的 IP 地址,Web App 直接连接 Pod。

例二:你的 web-app 运行在 web-namespace,而集群外部的用户需要通过 Nginx 访问它。Nginx 作为 Ingress 控制器,负责接收外部 HTTP 请求,并转发到 web-app。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:name: web-app-ingressnamespace: web-namespace
spec:ingressClassName: nginxrules:- host: mywebapp.example.comhttp:paths:- path: /pathType: Prefixbackend:service:name: web-appport:number: 80
  1. 用户访问 https://mywebapp.example.com:访问请求进入集群,通过 Ingress Controller(Nginx) 处理。
  2. Nginx 通过 web-app 的 ClusterIP 访问 Web App
    • CoreDNS 解析 web-app.web-namespace.svc.cluster.local,返回 ClusterIP(例如 10.96.0.30)。
    • Nginx 转发请求到 10.96.0.30:80(即 Web App Service)。
  3. Web App 处理请求
    • Web App 运行在某些 Pod 上,Service web-app 通过 kube-proxy 负载均衡到实际 Pod(假设 Pod 的 IP 是 192.168.1.100:8080)。
    • Web App 处理请求并返回响应。

附录

IDNA编码过程

RFC 3492中说明:国际化域名编码是一种称为Bootstring的更普遍的算法实例,它允许由部分基本的编码集合组成的字符串唯一表示由更大编码集合组成的任何字符串。配合统一码文本的特性,国际化域名编码定义了一般Bootstring算法的参数。

以,德语字符串"bücher"(书籍)为例,该字符串被编码为"bcher-kva":

  1. 首先,字符串中的所有ASCII字符将被直接从输入复制到输出,任何其他字符将被跳过。例如,"bücher"被复制为 “bcher”。如果有任何字符被复制(即输入中至少有一个ASCII字符),则在输出中附加一个ASCII连字符(例如,“bücher"复制为"bcher-”,其中"ü"不是ASCII字符,不发生复制)。

    由于连字符本身是ASCII字符,因此可以出现在输入中,并将被复制到输出中。这并不会引起歧义,因为如果输出包含连字符,被添加的连字符总是最后一个,这标志着ASCII字符的结束。

  2. 非 ASCII 字符按照 Unicode 码点值从小到大排序(如果字符重复,则按照其在字符串中的位置排序),然后将每个字符编码为一个数字。这个数字决定了该字符的插入位置以及具体的字符。

    例如:

    • ü 的 Unicode 码点是 0xFC(即 252)。
    • 计算 (252 - 127) = 124
    • bcher 中有 6 个可能的插入点(包括首尾)。
    • ü 被插入在 b 之后(索引 1)。
    • 计算编码数值 (6 × 124) + 1 = 745

    解码时,可以通过:

    • ⌊745 ÷ 6⌋ = 124
    • 745 mod 6 = 1

    恢复出 Unicode 码点 124 + 127 = 252(即 ü),以及其插入位置。

  3. 变长数字编码

    Punycode 使用通用的 变长整数 来表示这些编码值。例如,kva 代表数字 745,其编码过程如下:

    • 使用 36 进制 编码,其中:
      • a–z 对应 0–25
      • 0–9 对应 26–35
    • kva 解析为:
      • k = 10
      • v = 21
      • a = 0
    • 计算:
      • (10 × 1) + (21 × 35) + 0 = 745

    所以,最终:

    • bücherbcher-kva
  4. ACE 前缀(ASCII Compatible Encoding)

    为了防止普通域名中的连字符被误解为 Punycode 编码,国际化域名 采用前缀 xn--,即 ACE 编码
    bücher.tldxn–bcher-kva.tld

  5. 示例

    输入输入的国际化域名编码对输入的描述
    空字符
    aa-仅有ASCII字符,一个小写字母
    AA-仅有ASCII字符,一个大写字母
    33-仅有ASCII字符,一个数字
    ---仅有ASCII字符,一个连字符
    -----仅有ASCII字符,两个连字符
    LondonLondon-仅有ASCII字符,多于一个,没有连字符
    Lloyd-AtkinsonLloyd-Atkinson-仅有ASCII字符,一个连字符
    This has spacesThis has spaces-仅有ASCII字符,包含空格
    -> $1.00 <--> $1.00 <--仅有ASCII字符,多种符号混合
    а80a没有ASCII字符,一个西里尔字母
    ütda没有ASCII字符,一个拉丁字母补充-1中的字符
    αmxa没有ASCII字符,一个希腊字母
    fsq没有ASCII字符,一个CJK字符
    😉n28h没有ASCII字符,一个emoji字符
    αβγmxacd没有ASCII字符,多于一个字符
    MünchenMnchen-3ya混合字符,包含一个非ASCII字符
    Mnchen-3yaMnchen-3ya-München的两次国际化域名编码
    München-OstMnchen-Ost-9db混合字符串,包括一个非ASCII字符和一个连字符
    Bahnhof München-OstBahnhof Mnchen-Ost-u6b混合字符串,包括一个空格,一个连字符和一个非ASCII字符
    abæcdöefabcdef-qua4k混合字符串,包括两个非ASCII字符
    правда80aafi6cg俄语字符,不包括ASCII字符
    ยจฆฟคฏข22cdfh1b8fsa泰语字符,不包括ASCII字符
    도메인hq1bm8jm9l韩语字符,不包括ASCII字符
    ドメイン名例eckwd4c7cu47r2wf日语字符,不包括ASCII字符
    MajiでKoiする5秒前MajiKoi5-783gue6qz075azm5e日语字符,以及ASCII字符
    “bücher”bcher-kva8445foa混合的非ASCII字符(拉丁字母补充-1及CJK字符)

Punycode 使国际化域名(IDN)能够兼容现有的 DNS 系统,同时仍然支持 Unicode 字符集。这一技术在国际化互联网地址的实现中起到了关键作用。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com