域名系统 (DNS, Domain Name System)
什么是域名解析
主机名(hostname) 类似 www.xyz.com 这样的形式.
IP 地址则是类似 ::ffff:192.168.10.111 这样的形式.
主机名解析成 IP 地址, IP 地址解析成主机名, 这种机制称为 域名系统(DNS).
DNS 用来给连接到 Internet 的计算机和系统指定一个名字.
当程序要连接到远端的计算机, 如 www.xyz.com, 就需要找到 www.xyz.com 的 IP 地址.
这就是域名解析, 之前都是通过调用 getaddrinfo() 来实现.
调用 getaddrinfo() 后, 操作系统的操作
查看 www.xyz.com 的 IP 地址是否已知( 即, 本地缓存中是否已存在相关记录).
如果最近曾经使用过该主机名, 允许操作系统将其 IP 在 本地缓存(local cache) 保存一段时间(TTL, time-to-live).
如果在本地缓存中未找到, 操作系统会向 DNS 服务器查询.
DNS 服务器通常由 互联网服务提供商(ISP) 提供, 也存在很多公共的 DNS 服务器.
服务器收到一个查询请求, 同样先查看服务器的本地缓存.
这很有用, 因为可能有大量的系统依赖同一台服务器.
如果一分钟内服务器收到 1000 次请求, 都是查询 xyz.com, 只需对第一次请求进行域名解析, 999 次简单的返回本地缓存内已存储的记录即可.
如果服务器缓存内没有请求的 DNS 记录, 就需要去其他服务器查询, 直到连接到负责目标系统的服务器为止.
客户 A 的 DNS 服务器尝试解析 www.xyz.com
- A 向其 DNS 服务器请求 www.xyz.com 的 IP 地址
- A 的服务器连接到一台根服务器请求 www.xyz.com 对应 IP
- 根服务器指示 A 的 DNS 服务器 去请求 .com 的 DNS 服务器
- A 的 DNS 服务器连接到负责 .com 的 DNS 服务器, 请求 www.xyz.com 对应的 IP
- .com 的 DNS 服务器给 A 的 DNS 服务器提供另一台 DNS 服务器的地址, xyz.com 的 DNS 服务器
- A 的 DNS 服务器连接到 xyz.com 的 DNS 服务器, 请求 www.xyz.com 的 DNS 记录
- xyz.com 的 DNS 服务器返回 www.xyz.com 的地址给 A 的 DNS 服务器
- A 的 DNS 服务器将该地址转发给 A
查询过程共发送了 8 条消息, 而实际上整个过程可能还要更长, 这体现了 DNS 缓存的重要性.
DNS 记录的类型
记录类型 | 类型 ID | 描述 |
---|---|---|
A | 1 | IPv4 地址记录 |
AAAA | 28 | IPv6 地址记录 |
MX | 15 | 邮件交换记录 |
TXT | 16 | 文字描述记录 |
CNAME | 5 | 别名 |
* (ALL, ANY) | 255 | 所有缓存的记录 |
TXT, 可以存储关于主机名的任意信息, 实践中, 设置域名的所有权, 邮件发送指引.
CNAME, 给特定主机名提供别名.
xyz.com 和 www.xyz.com 应当指向相同的地址.
给 xyz.com 添加 A 类 和 AAAA 类记录.
给 www.xyz.com 添加 CNAME 类记录, 指向 xyz.com.
DNS 客户不会直接查询 CNAME 记录, 而是请求 www.xyz.com 的 A 类和 AAAA 类记录.
DNS 服务器会将 CNAME 记录作为回应, 指向 xyz.com.
DNS 客户再继续查询 xyz.com.
伪记录(pseudo-record) 类型 * (或 ALL, ANY).
为当前查询返回缓存内所有已知的记录.
一个主机名可能有多个相同类型的记录与之关联.
即是说, xyz.com 可以有多个 A 类记录, 即关联多个 IPv4 地址.
DNS 安全
Domain Name System Security Extensions(DNSSEC)
提供数据认证, 让 DNS 客户知道 DNS 回应经过了认证, 但无法阻止窃听.
DNS over HTTPS(DoH), 通过 HTTPS 进行域名解析.
HTTPS 提供较强的安全保障, 包括阻止劫持.
使用不安全的 DNS 有什么潜在影响?
如果 DNS 未经过认证, 攻击者可能谎报域名对应的 IP 地址.
受害者以为连接到了 xyz.com, 但实际上连接到的 IP 地址上运行着攻击者控制的恶意的服务器.
使用 HTTPS 会好一些, HTTPS 提供服务器身份的认证.
但如果 DNS 未经过加密, DNS 查询就仍是易于窃听的.
窃听者可以知道被窃听者访问过哪些网站, 连接过哪些服务器.
关于 getaddrinfo(), 将文本形式的地址或名称转换成 struct addrinfo 结构
- /* 函数原型 */
- int getaddrinfo(
- const char *node,
- const char *service,
- const struct addrinfo *hints,
- struct addrinfo **res
- );
node | 指定主机名或地址( 字符串形式) |
如 xyz.com, 192.168.0.1, ::1 | |
service | 指定服务或端口号( 字符串形式) |
如 http, 80, 0(NULL) | |
hints | 指向 struct addrinfo 类型的指针, 指定地址的选项 |
可以传入 NULL, 代表默认值, 不同系统默认值可能不同 | |
res | 指向 struct addrinfo 类型的链表的指针 |
返回 getaddrinfo() 函数找到的地址 |
- /* 关于 struct addrinfo 结构, 不同操作系统内容会有变化 */
- struct addrinfo {
- int ai_flags,
- int ai_family,
- int ai_socktype,
- int ai_protocal,
- socklen_t ai_addrlen,
- struct sockaddr *ai_addr,
- char *ai_canonname,
- struct addrinfo *ai_next
- }
getaddrinfo() 检查 *hints 的 4 个字段, ai_family, ai_socktype, ai_protocal, ai_flags.
*hints 其他字段应置为 0.
ai_family | AF_INET 代表 IPv4 | |
AF_INET6 代表 IPv6 | ||
AF_UNSPEC(即 0) 代表 任意 | ||
ai_socktype | SOCK_STREAM 代表使用 TCP | |
SOCK_DGRAM 代表使用 UDP | ||
0 表示任意 | ||
ai_protocal | 应该置 0, 表明任意协议 | |
TCP 不是唯一被支持的流协议, UDP 也不是唯一被支持的数据报协议 | ||
ai_protocol 用于消除歧义 | ||
ai_flags | 指定更多的选项, 使用位或形式, 即 flag1 | flag2 | ... | |
AI_NUMERICHOST | node 参数为 IP 地址形式, 不是域名形式, 可以用来避免域名查询 | |
AI_NUMERICSERV | service 参数只接受端口号形式, 不接受服务名形式 | |
AI_ALL | 请求全部类型的地址, IPv4 和 IPv6 | |
AI_ADDRCONFIG | 返回的 IP 地址类型必须与本地接口的配置一致 | |
AI_PASSIVE | getaddrinfo() 的参数 node 为 0, 请求通配地址(wildcard address) | |
node 不为 0, 则没有任何效果 | ||
通配地址, 一个本地地址, 可以接受连接, 服务器上和 bind() 配合使用 |
关于 getnameinfo(), 将 struct addrinfo 转换成文本形式
- /* 函数声明 */
- int getnameinfo(
- const struct sockaddr *addr, socklen_t addrlen,
- char *host, socklen_t hostlen,
- char *serv, socklen_t servlen,
- int flags
- );
前两个参数是 struct addrinfo 结构的两个字段, ai_addr 和 ai_addrlen.
host, hostlen, 指定一个字符缓冲区及其大小, 用于存储主机名或 IP 地址.
serv, servlen, 指定一个字符缓冲区及其大小, 用于存储服务名或端口号.
关于 flags, 使用位或形式, flag1 | flag2 | ...
flags | NI_NAMEREQD | 要求返回主机名, 不是地址. |
如果主机名无法确定, 则会出错. | ||
NI_DGRAM | 指定服务是基于 UDP(UDP-based) 的. | |
如果设置了 NI_NUMERICSERV, NI_DGRAM 会被忽略. | ||
NI_NUMERICHOST | 要求返回 IP 地址, 不是主机名 | |
NI_NUMERICSERV | 要求返回端口号, 不是服务名 |
lookup.dns.c
- /* lookup.dns.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include "network.h"
- int main(int argc, char ** argv) {
- if (argc != 2) {
- fprintf(stderr, "Usage: %s hostname\n", argv[0]);
- return EXIT_FAILURE;
- }
- printf("Resolving hostname '%s' ...\n", argv[1]);
- struct addrinfo hints;
- memset(&hints, 0, sizeof(hints));
- /* 要求返回所有可用的 IP 地址, IPv4 和 IPv6 */
- hints.ai_flags = AI_ALL;
- struct addrinfo *peer_addr;
- /*
- 第一个参数传入 argv[1]
- 如果 argv[1] 是个名字, 如 xyz.com, 系统会进行 DNS 查询( 假设 xyz.com 在本地缓存中不存在)
- 如果 argv[1] 是个地址, 如 192.168.1.1, 函数对 peer_addr 进行填充, 不进行 DNS 查询
- 第二个参数传入 NULL, 因为这段程序不关心端口号
- */
- if (getaddrinfo(argv[1], NULL, &hints, &peer_addr)) {
- fprintf(stderr, "getaddrinfo() failed, errno: %d, %s\n", errno, strerror(errno));
- return EXIT_FAILURE;
- }
- printf("Remote address:\n");
- struct addrinfo * addr = peer_addr;
- while (addr != NULL) {
- char addr_buff[100];
- getnameinfo(
- addr->ai_addr, addr->ai_addrlen,
- addr_buff, sizeof(addr_buff),
- NULL, 0,
- NI_NUMERICHOST
- );
- printf("\t%s\n", addr_buff);
- addr = addr->ai_next;
- }
- freeaddrinfo(peer_addr);
- return EXIT_SUCCESS;
- }