一、项目背景详细介绍

域名解析(DNS,Domain Name System)是互联网最基础的服务之一:将人类可读的域名(例如 www.example.com)转换为计算机可用的 IP 地址(例如 93.184.216.34)。几乎所有互联网通信都依赖 DNS。作为系统/网络编程、运维与安全工程师的基础技能,理解并实现域名解析有助于掌握以下关键点:

  • 套接字(socket)编程基础;
  • 使用系统库(如 getaddrinfo)进行易用高层解析;
  • 理解 DNS 协议(基于 UDP 的报文格式)并自己实现一个简易 DNS 客户端以加深理解;
  • IPv4 与 IPv6 的区别、反向解析(PTR)、超时、重试与报错处理;
  • 在受控环境下测试与调试(如使用本地 DNS 服务器或公共 DNS)。

本项目通过两种实现方式讲解域名解析:

  1. 系统接口法(推荐, 简单可靠):使用标准库函数 getaddrinfo() / getnameinfo() 实现同步解析。优点:跨平台、支持 IPv4/IPv6、自动处理搜索域和服务名。适合大多数应用场景。
  2. 自实现 DNS 客户端(进阶,学习用):直接构造 DNS 请求报文通过 UDP 发送到 DNS 服务器(如 8.8.8.8),解析返回的资源记录(A、AAAA、CNAME)。优点:深入理解 DNS 协议、可以自定义查询行为、用于教学和调试。缺点:需处理更多协议细节(字节序、报文压缩、超时重传等)。

二、项目需求详细介绍

功能性需求

实现一个命令行工具 resolver,支持下列功能:

  • 使用系统接口解析域名(显示 IPv4 与 IPv6 地址)。
  • 使用自实现 DNS 客户端向指定 DNS 服务器(可配置)发送查询并解析 A/AAAA/CNAME 记录。
  • 支持正向解析(域名 → IP)与反向解析(IP → 域名)。
  • 对错误(找不到主机、超时、网络不可达等)给出清晰提示。
  • 在自实现 DNS 客户端中,处理简单的 DNS 名称压缩,并支持超时与重试(基础级)。

非功能性需求

  • 代码用纯 C 实现(兼顾可移植性),在 Linux 下可直接编译。
  • 结构清晰,函数注释详尽,便于教学演示。
  • 主体代码放在单一代码块内,内部用注释分隔“模块/文件”。
  • 提供示例用法与测试样例。

三、相关技术详细介绍

1. 系统 DNS 接口(getaddrinfo/getnameinfo)

getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res):跨 IPv4/IPv6 的推荐方式。返回链表形式的 addrinfo,其中包含 sockaddr 可直接用于 connect 或可转换为字符串。

getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags):用于反向解析(IP → 主机名)或将 sockaddr 转换为数字字符串。

优点:处理搜索域、可并发安全(线程安全实现依赖平台),推荐使用。

2. DNS 协议(报文结构,简要)

DNS 报文(RFC 1035)大体结构:

  • 16-bit ID
  • 16-bit flags
  • 16-bit QDCOUNT(问题条目数)
  • 16-bit ANCOUNT(回答条目数)
  • 16-bit NSCOUNT(权威记录数)
  • 16-bit ARCOUNT(额外记录数)
  • 问题(QNAME,QTYPE,QCLASS)
  • 资源记录(NAME,TYPE,CLASS,TTL,RDLENGTH,RDATA)

QNAME 为标签序列(每段前缀长度字节,末尾 0)。返回报文中 RDATA 对于 A 为 4 字节 IPv4 地址、AAAA 为 16 字节 IPv6 地址。注意 DNS 报文中会有名称压缩(两个字节的指针以 11 开头);实现时需解析压缩格式。

3. UDP 套接字编程与超时处理

自实现 DNS 客户端使用 UDP 套接字向 DNS 服务器发送查询并等待响应;需设置接收超时(setsockopt(..., SO_RCVTIMEO, ...)),并在超时或错误时实现重试机制(例如最多重试 3 次)。

四、实现思路详细介绍

本文实现分两部分(两种方法):

方法一:系统接口法(getaddrinfo)

  • 创建 hints:设置 ai_family = AF_UNSPEC(同时支持 IPv4 和 IPv6),ai_socktype = SOCK_STREAM(或 0),ai_flags = AI_CANONNAME(请求返回规范名)。
  • 调用 getaddrinfo(domain, NULL, &hints, &res), 遍历 res 链表,使用 getnameinfo 或 inet_ntop 打印 IP 地址字符串。
  • 反向解析使用 getnameinfo

该方法短小精悍且功能完备。

方法二:自实现 DNS 客户端(UDP)

构造 DNS 查询报文:

随机 ID(16-bit)

flags 设置为标准递归查询(Recursion Desired)

QDCOUNT = 1

构造 QNAME(标签序列)

QTYPE = A(1)或 AAAA(28)

QCLASS = IN(1)

发送到 DNS 服务器 UDP 53 端口。

等待响应,解析头部获取 ANCOUNT,解析资源记录:

  • 解析 NAME(处理压缩)
  • 解析 TYPE、CLASS、TTL、RDLENGTH、RDATA
  • 对 A / AAAA / CNAME 做输出
  • 处理超时与重试,如 2 秒超时,最多重试 3 次。
  • 注意网络字节序(htons/ntohs)。

实现时会包含名称解压函数 dns_name_unpack(),用于把压缩的域名解析为可读字符串。

五、完整实现代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

/***************************************************************

 * 文件:resolver.c

 * 功能:C 语言实现域名解析(两种方式)

 *   - 方法 A:使用系统接口 getaddrinfo/getnameinfo(推荐)

 *   - 方法 B:自实现简易 DNS 客户端(UDP 查询 A / AAAA / CNAME)

 *

 * 编译(Linux):

 *   gcc resolver.c -o resolver

 *

 * 用法示例:

 *   ./resolver sys www.example.com         # 使用系统接口解析

 *   ./resolver dns 8.8.8.8 www.example.com # 使用自定义 DNS 客户端,指定 DNS 服务器

 *   ./resolver reverse sys 93.184.216.34   # 反向解析(IP->域名)使用系统接口

 *

 ***************************************************************/

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <errno.h>

#include <unistd.h>

#include <arpa/inet.h>

#include <netdb.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <time.h>

#include <sys/time.h>

/* -----------------------------

   通用工具函数与定义

   ----------------------------- */

#define MAXBUF 512

static void pprint_addrinfo(struct addrinfo *res) {

    char host[NI_MAXHOST];

    for (struct addrinfo *p = res; p != NULL; p = p->ai_next) {

        void *addr;

        const char *ipver;

        if (p->ai_family == AF_INET) {

            struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;

            addr = &(ipv4->sin_addr);

            ipver = "IPv4";

        } else if (p->ai_family == AF_INET6) {

            struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;

            addr = &(ipv6->sin6_addr);

            ipver = "IPv6";

        } else {

            continue;

        }

        if (inet_ntop(p->ai_family, addr, host, sizeof(host)) == NULL) {

            strncpy(host, "unknown", sizeof(host));

            host[sizeof(host)-1] = 0;

        }

        printf("  %-4s  %s\n", ipver, host);

    }

}

/* -----------------------------

   方法 A:使用系统接口 getaddrinfo / getnameinfo

   ----------------------------- */

static int resolve_with_getaddrinfo(const char *name) {

    struct addrinfo hints, *res;

    int rv;

    memset(&hints, 0, sizeof(hints));

    hints.ai_family = AF_UNSPEC;    // 支持 IPv4 和 IPv6

    hints.ai_socktype = 0;          // 任意 socket 类型

    hints.ai_flags = AI_CANONNAME;  // 返回规范名(如可用)

    rv = getaddrinfo(name, NULL, &hints, &res);

    if (rv != 0) {

        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));

        return 1;

    }

    printf("Canonical name: %s\n", res->ai_canonname ? res->ai_canonname : "(none)");

    printf("Addresses for %s:\n", name);

    pprint_addrinfo(res);

    freeaddrinfo(res);

    return 0;

}

/* 反向解析:IP -> 主机名 */

static int reverse_with_getnameinfo(const char *ipstr) {

    struct sockaddr_storage sa;

    socklen_t sa_len;

    char host[NI_MAXHOST];

    memset(&sa, 0, sizeof(sa));

    if (strchr(ipstr, ':')) {

        // IPv6

        struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&sa;

        sa6->sin6_family = AF_INET6;

        if (inet_pton(AF_INET6, ipstr, &(sa6->sin6_addr)) != 1) {

            fprintf(stderr, "Invalid IPv6 address: %s\n", ipstr);

            return 1;

        }

        sa_len = sizeof(struct sockaddr_in6);

    } else {

        // IPv4

        struct sockaddr_in *sa4 = (struct sockaddr_in *)&sa;

        sa4->sin_family = AF_INET;

        if (inet_pton(AF_INET, ipstr, &(sa4->sin_addr)) != 1) {

            fprintf(stderr, "Invalid IPv4 address: %s\n", ipstr);

            return 1;

        }

        sa_len = sizeof(struct sockaddr_in);

    }

    int rv = getnameinfo((struct sockaddr *)&sa, sa_len, host, sizeof(host), NULL, 0, NI_NAMEREQD);

    if (rv != 0) {

        fprintf(stderr, "getnameinfo: %s\n", gai_strerror(rv));

        return 1;

    }

    printf("Reverse lookup: %s -> %s\n", ipstr, host);

    return 0;

}

/* -----------------------------

   方法 B:自实现简易 DNS 客户端(UDP)

   仅支持查询 A(1) / AAAA(28) / CNAME (5)

   ----------------------------- */

/* DNS header: 12 bytes */

#pragma pack(push, 1)

struct dns_header {

    unsigned short id;

    unsigned short flags;

    unsigned short qdcount;

    unsigned short ancount;

    unsigned short nscount;

    unsigned short arcount;

};

#pragma pack(pop)

/* DNS question tail (type & class) */

struct dns_question_tail {

    unsigned short qtype;

    unsigned short qclass;

};

/* 资源记录固定头部(不含可变长度 NAME 与 RDATA) */

#pragma pack(push,1)

struct dns_rr_fixed {

    unsigned short type;

    unsigned short class;

    unsigned int ttl;

    unsigned short rdlength;

};

#pragma pack(pop)

/* 将域名从点分格式转换为 DNS 报文的标签格式:

   e.g., "www.example.com" -> [3]www[7]example[3]com[0]

   返回写入的字节数到 buf(不超过 buflen)。

*/

static int dns_name_pack(const char *name, unsigned char *buf, int buflen) {

    int nlen = strlen(name);

    if (nlen == 0) {

        if (buflen < 1) return -1;

        buf[0] = 0;

        return 1;

    }

    int pos = 0;

    const char *label = name;

    const char *p = name;

    while (1) {

        if (*p == '.' || *p == '\0') {

            int len = p - label;

            if (len > 63) return -1; // label too long

            if (pos + 1 + len >= buflen) return -1;

            buf[pos++] = (unsigned char)len;

            if (len > 0) {

                memcpy(&buf[pos], label, len);

                pos += len;

            }

            if (*p == '\0') {

                // terminate with zero

                if (pos >= buflen) return -1;

                buf[pos++] = 0;

                break;

            }

            label = p + 1;

        }

        p++;

    }

    return pos;

}

/* 解析 DNS 报文中的名称(包含指针压缩)

   packet:完整报文起始地址

   pktlen:报文总长度

   offset:当前读取偏移(输入时指向名称开始),函数结束时 offset 会更新到名称后的第一个字节

   out:输出缓冲区存放解析出的点分格式字符串

   outlen:输出长度

   返回:0 成功,-1 失败

*/

static int dns_name_unpack(unsigned char *packet, int pktlen, int *offset, char *out, int outlen) {

    int orig_offset = *offset;

    int pos = 0;

    int jumped = 0;

    int max_jumps = 0;

    int cur = *offset;

    while (cur < pktlen) {

        unsigned char len = packet[cur];

        if (len == 0) {

            // end of name

            if (!jumped) *offset = cur + 1;

            if (pos == 0) {

                // root

                if (pos + 1 > outlen) return -1;

                out[pos] = '\0';

            } else {

                if (pos + 1 > outlen) return -1;

                out[pos] = '\0';

            }

            return 0;

        }

        if ((len & 0xC0) == 0xC0) {

            // pointer: next byte + (len & 0x3F) << 8

            if (cur + 1 >= pktlen) return -1;

            int b2 = packet[cur + 1];

            int pointer = ((len & 0x3F) << 8) | b2;

            if (pointer >= pktlen) return -1;

            if (!jumped) *offset = cur + 2;

            cur = pointer;

            jumped = 1;

            if (++max_jumps > 10) return -1; // avoid loops

            continue;

        } else {

            // label

            cur++;

            if (cur + len > pktlen) return -1;

            if (pos + len + 1 >= outlen) return -1;

            memcpy(out + pos, packet + cur, len);

            pos += len;

            out[pos++] = '.';

            cur += len;

        }

    }

    return -1;

}

/* 构造并发送 DNS 查询(type: 1=A, 28=AAAA)

   dns_server: dotted IP string, e.g., "8.8.8.8"

   name: 域名

   type: qtype (1/A, 28/AAAA)

   timeout_sec: 接收超时时间(秒)

   返回:0 成功(并将打印解析结果),非0表示失败

*/

static int dns_query_simple(const char *dns_server, const char *name, unsigned short qtype, int timeout_sec) {

    unsigned char buf[512];

    memset(buf, 0, sizeof(buf));

    struct dns_header hdr;

    memset(&hdr, 0, sizeof(hdr));

    srand((unsigned int)time(NULL));

    hdr.id = (unsigned short) (rand() & 0xFFFF);

    hdr.flags = htons(0x0100); // standard query, recursion desired

    hdr.qdcount = htons(1);

    // 写 header

    memcpy(buf, &hdr, sizeof(hdr));

    int offset = sizeof(hdr);

    // 写 QNAME

    int n = dns_name_pack(name, buf + offset, sizeof(buf) - offset);

    if (n < 0) {

        fprintf(stderr, "dns_name_pack failed\n");

        return 1;

    }

    offset += n;

    // 写 QTYPE & QCLASS

    struct dns_question_tail qt;

    qt.qtype = htons(qtype);

    qt.qclass = htons(1); // IN

    memcpy(buf + offset, &qt, sizeof(qt));

    offset += sizeof(qt);

    // 发送 UDP 包

    int sock = socket(AF_INET, SOCK_DGRAM, 0);

    if (sock < 0) {

        perror("socket");

        return 1;

    }

    struct sockaddr_in serv;

    memset(&serv, 0, sizeof(serv));

    serv.sin_family = AF_INET;

    serv.sin_port = htons(53);

    if (inet_pton(AF_INET, dns_server, &serv.sin_addr) != 1) {

        fprintf(stderr, "Invalid DNS server IP: %s\n", dns_server);

        close(sock);

        return 1;

    }

    // 发送并等待回复(带重试)

    int tries = 3;

    int rv = 1;

    for (int t = 0; t < tries; t++) {

        ssize_t sent = sendto(sock, buf, offset, 0, (struct sockaddr*)&serv, sizeof(serv));

        if (sent != offset) {

            perror("sendto");

            continue;

        }

        // 设置 recv 超时

        struct timeval tv;

        tv.tv_sec = timeout_sec;

        tv.tv_usec = 0;

        setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));

        unsigned char resp[4096];

        struct sockaddr_in from;

        socklen_t fromlen = sizeof(from);

        ssize_t rlen = recvfrom(sock, resp, sizeof(resp), 0, (struct sockaddr*)&from, &fromlen);

        if (rlen < 0) {

            if (errno == EAGAIN || errno == EWOULDBLOCK) {

                // timeout

                if (t == tries - 1) {

                    fprintf(stderr, "DNS query timeout\n");

                } else {

                    // retry

                    continue;

                }

            } else {

                perror("recvfrom");

            }

            continue;

        }

        // parse response

        if (rlen < (ssize_t)sizeof(struct dns_header)) {

            fprintf(stderr, "DNS response too short\n");

            continue;

        }

        struct dns_header rhdr;

        memcpy(&rhdr, resp, sizeof(rhdr));

        unsigned short qdcount = ntohs(rhdr.qdcount);

        unsigned short ancount = ntohs(rhdr.ancount);

        // skip questions

        int roff = sizeof(struct dns_header);

        for (int i = 0; i < qdcount; i++) {

            char qname[256];

            if (dns_name_unpack(resp, rlen, &roff, qname, sizeof(qname)) != 0) {

                fprintf(stderr, "Failed to unpack question name\n");

                break;

            }

            // skip qtype & qclass

            if (roff + sizeof(struct dns_question_tail) > rlen) break;

            roff += sizeof(struct dns_question_tail);

        }

        // parse answers

        printf("Answers (%d):\n", ancount);

        for (int i = 0; i < ancount; i++) {

            char aname[512];

            if (dns_name_unpack(resp, rlen, &roff, aname, sizeof(aname)) != 0) {

                fprintf(stderr, "Failed to unpack answer name\n");

                break;

            }

            if (roff + sizeof(struct dns_rr_fixed) > rlen) {

                fprintf(stderr, "Truncated RR header\n");

                break;

            }

            struct dns_rr_fixed rrf;

            memcpy(&rrf, resp + roff, sizeof(rrf));

            roff += sizeof(rrf);

            unsigned short type = ntohs(rrf.type);

            unsigned short rclass = ntohs(rrf.class);

            unsigned short rdlen = ntohs(rrf.rdlength);

            if (roff + rdlen > rlen) {

                fprintf(stderr, "Truncated RDATA\n");

                break;

            }

            if (type == 1 && rdlen == 4) {

                // A

                char addrbuf[INET_ADDRSTRLEN];

                inet_ntop(AF_INET, resp + roff, addrbuf, sizeof(addrbuf));

                printf("  NAME: %s  TYPE=A  ADDR=%s\n", aname, addrbuf);

            } else if (type == 28 && rdlen == 16) {

                // AAAA

                char addrbuf[INET6_ADDRSTRLEN];

                inet_ntop(AF_INET6, resp + roff, addrbuf, sizeof(addrbuf));

                printf("  NAME: %s  TYPE=AAAA ADDR=%s\n", aname, addrbuf);

            } else if (type == 5) {

                // CNAME (rdata is a name)

                int tmp_off = roff;

                char cname[512];

                if (dns_name_unpack(resp, rlen, &tmp_off, cname, sizeof(cname)) == 0) {

                    printf("  NAME: %s  TYPE=CNAME RDATA=%s\n", aname, cname);

                } else {

                    printf("  NAME: %s  TYPE=CNAME (unpack failed)\n", aname);

                }

            } else {

                printf("  NAME: %s  TYPE=%d  (rdlen=%d bytes)\n", aname, type, rdlen);

            }

            roff += rdlen;

        }

        rv = 0; // success

        break;

    }

    close(sock);

    return rv;

}

/* -----------------------------

   简单命令行接口

   支持:

     resolver sys <domain>

     resolver reverse sys <ip>

     resolver dns <dns_server> <domain>

   ----------------------------- */

int main(int argc, char *argv[]) {

    if (argc < 3) {

        fprintf(stderr, "Usage:\n");

        fprintf(stderr, "  %s sys <domain>                  # use system resolver\n", argv[0]);

        fprintf(stderr, "  %s reverse sys <ip>              # reverse lookup via system resolver\n", argv[0]);

        fprintf(stderr, "  %s dns <dns_server> <domain>     # use custom DNS client\n", argv[0]);

        return 1;

    }

    if (strcmp(argv[1], "sys") == 0) {

        // system resolver

        return resolve_with_getaddrinfo(argv[2]);

    } else if (strcmp(argv[1], "reverse") == 0 && argc >= 4 && strcmp(argv[2], "sys") == 0) {

        return reverse_with_getnameinfo(argv[3]);

    } else if (strcmp(argv[1], "dns") == 0 && argc >= 4) {

        const char *dns_server = argv[2];

        const char *domain = argv[3];

        // query A and AAAA

        printf("Query A records via DNS server %s for %s\n", dns_server, domain);

        dns_query_simple(dns_server, domain, 1, 2);

        printf("\nQuery AAAA records via DNS server %s for %s\n", dns_server, domain);

        dns_query_simple(dns_server, domain, 28, 2);

        return 0;

    } else {

        fprintf(stderr, "Unknown command or insufficient args\n");

        return 1;

    }

}

到此这篇关于基于C语言实现域名解析(附带源码)的文章就介绍到这了

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐