C语言手写简易 DNS 客户端(接收部分)

发布于:2025-07-13 ⋅ 阅读:(14) ⋅ 点赞:(0)

本文通过纯 C 语言手动构造 DNS 请求报文,使用 UDP 协议发送到公共 DNS 服务器,并接收响应,完整演示 DNS 请求流程。

主流程:dns_client_commit()

这是整个流程的核心函数,下面我们按顺序拆解每一步的逻辑,尤其突出发送 sendto 与接收 recvfrom 的设计思路和实现

第一步:创建 UDP 套接字

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
    return -1;
}
  • 使用 IPv4(AF_INET)+ UDP(SOCK_DGRAM)创建 socket;

  • 返回值 sockfd 是文件描述符,用于后续发送接收;

  • 创建失败时直接返回错误。

第二步:配置目标服务器地址

struct sockaddr_in servaddr = {0};
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(DNS_SERVER_PORT);
servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP);
  • 填写目标 DNS 服务器信息,本例中使用的是 114.114.114.114

  • 注意端口是 53,必须使用 htons() 转换为网络字节序;

  • IP 地址也需通过 inet_addr() 转换。

第三步(可选):connect() 设置默认目标地址

connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));

虽然 UDP 不需要建立连接,但 connect() 可以用来为 socket 设置默认收发对象,使 send()recv() 也能直接用。

不过后续我们仍然使用 sendto()recvfrom(),所以这步不是必须的。

第四步:构造 DNS 请求结构体

struct dns_header header = {0};
dns_create_header(&header);

struct dns_question question = {0};
dns_create_question(&question, domain);
  • dns_create_header 随机生成请求 ID,设置标准查询标志和问题数;

  • dns_create_question 将域名编码成符合 DNS 协议的 QNAME 格式,并设置查询类型(A)和类(IN);

第五步:拼接报文

char request[1024] = {0};
int length = dns_build_request(&header, &question, request, sizeof(request));
  • 将 header + QNAME + QTYPE + QCLASS 依次拷贝到 request 中;

  • 返回整个 DNS 报文的长度(以字节计);

  • 最终得到一个可以直接发送的字节数组。

第六步:使用 sendto() 发送数据包

int slen = sendto(sockfd, request, length, 0,
                  (struct sockaddr*)&servaddr,
                  sizeof(struct sockaddr_in));
  • 把完整请求通过 UDP 发送给服务器;

  • sendto() 第一个参数是 socket;

  • 最后两个参数是目标地址和长度。

第七步:使用 recvfrom() 接收响应

char response[1024] = {0};
struct sockaddr_in addr;
size_t addr_len = sizeof(struct sockaddr_in);

int n = recvfrom(sockfd, response, sizeof(response), 0,
                 (struct sockaddr*)&addr,
                 (socklen_t*)&addr_len);
  • recvfrom() 会阻塞,直到收到服务器响应为止;

  • 将响应保存到 response 中;

  • n 表示收到的字节数。

第八步:输出结果(测试用)

printf("recvfrom : %d, %s\n", n, response);

DNS 客户端请求流程图(文字版)

┌────────────────────────────────────────────┐
│                程序入口 main()             │
└────────────┬───────────────────────────────┘
             │
             ▼
┌────────────────────────────────────────────┐
│ 判断命令行参数个数是否 < 2?                │
│ 是 → 直接 return -1                         │
│ 否 → 提取域名参数 argv[1]                  │
└────────────┬───────────────────────────────┘
             │
             ▼
┌────────────────────────────────────────────┐
│        调用 dns_client_commit(domain)       │
└────────────┬───────────────────────────────┘
             │
             ▼
┌────────────────────────────────────────────┐
│ 创建 UDP socket(socket(AF_INET, SOCK_DGRAM))│
│ 创建失败 → return -1                       │
└────────────┬───────────────────────────────┘
             │
             ▼
┌────────────────────────────────────────────┐
│ 设置服务器地址 servaddr                    │
│  - IP: 114.114.114.114                     │
│  - 端口: 53                                │
└────────────┬───────────────────────────────┘
             │
             ▼
┌────────────────────────────────────────────┐
│ 可选:connect(sockfd, servaddr)            │
└────────────┬───────────────────────────────┘
             │
             ▼
┌────────────────────────────────────────────┐
│ 调用 dns_create_header(&header)            │
│  - 填写请求 ID(随机)                    │
│  - 设置 Flags = 0x0100                    │
│  - 设置 Questions = 1                     │
└────────────┬───────────────────────────────┘
             │
             ▼
┌────────────────────────────────────────────┐
│ 调用 dns_create_question(&question, domain)│
│  - 编码域名为 QNAME(带长度前缀 + 0 结尾) │
│  - 设置 QTYPE = 1(A记录)                 │
│  - 设置 QCLASS = 1(IN)                   │
└────────────┬───────────────────────────────┘
             │
             ▼
┌────────────────────────────────────────────┐
│ 调用 dns_build_request(...) 构建请求报文   │
│  - 拷贝 header → request                   │
│  - 拷贝 QNAME → request                    │
│  - 拷贝 QTYPE → request                    │
│  - 拷贝 QCLASS → request                   │
│ 返回 request 长度 length                   │
└────────────┬───────────────────────────────┘
             │
             ▼
┌────────────────────────────────────────────┐
│ 使用 sendto(...) 发送请求报文给 DNS 服务器 │
│ 参数:socket、request、servaddr            │
└────────────┬───────────────────────────────┘
             │
             ▼
┌────────────────────────────────────────────┐
│ 使用 recvfrom(...) 等待接收服务器响应      │
│ 将响应数据保存至 response[] 中             │
│ 返回接收的字节数 n                          │
└────────────┬───────────────────────────────┘
             │
             ▼
┌────────────────────────────────────────────┐
│ 打印响应长度和部分内容(简单调试)         │
│ printf("recvfrom : %d, %s\n", n, response); │
└────────────┬───────────────────────────────┘
             │
             ▼
┌────────────────────────────────────────────┐
│           函数结束,返回 n 字节长度         │
└────────────────────────────────────────────┘

完整代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <sys/socket.h>       // socket相关函数
#include <netinet/in.h>       // sockaddr_in结构体
#include <unistd.h>           // close()

// DNS服务器配置(可换成8.8.8.8等)
#define DNS_SERVER_PORT     53  
#define DNS_SERVER_IP       "114.114.114.114"

// =======================
// DNS 报文头部结构体(固定12字节)
// =======================
struct dns_header {
    unsigned short id;         // 标识符(客户端生成,响应中应一致)
    unsigned short flags;      // 标志位(是否递归、是否响应等)
    unsigned short questions;  // 问题数量(通常为1)
    unsigned short answers;    // 回答资源记录数量(请求为0)
    unsigned short authority;  // 授权资源记录数量
    unsigned short additional; // 附加资源记录数量
};

// =======================
// DNS 问题部分结构体(QNAME + QTYPE + QCLASS)
// =======================
struct dns_question {
    int length;                // QNAME部分长度(带点标签形式)
    unsigned short qtype;      // 查询类型(A记录为1)
    unsigned short qclass;     // 查询类(IN类为1)
    unsigned char *name;       // 编码后的QNAME(域名)
};

// =======================
// 构造DNS报文头部
// =======================
int dns_create_header(struct dns_header *header) {
    if (header == NULL) return -1;

    memset(header, 0, sizeof(struct dns_header));

    // 使用时间种子生成随机 ID
    srand((unsigned int)time(NULL));
    header->id = (unsigned short)rand();     // 随机标识符
    header->flags = htons(0x0100);           // 标准查询 + 递归请求
    header->questions = htons(1);            // 只查询一个问题

    return 0;
}

// =======================
// 构造DNS问题部分:编码域名 + QTYPE + QCLASS
// =======================
int dns_create_question(struct dns_question *question, const char *hostname) {
    if (question == NULL || hostname == NULL) return -1;

    memset(question, 0, sizeof(struct dns_question));

    size_t hostlen = strlen(hostname);
    question->name = (unsigned char*)malloc(hostlen + 2);  // 预留一个结尾的 0x00
    if (question->name == NULL) return -2;

    question->length = (int)hostlen + 2;
    question->qtype = htons(1);    // 查询A记录(IPv4地址)
    question->qclass = htons(1);   // IN类(Internet)

    const char delim[2] = ".";
    unsigned char *qname = question->name;

    // 拷贝hostname,避免破坏原始字符串
    char *hostname_dup = strdup(hostname);
    if (hostname_dup == NULL) {
        free(question->name);
        return -3;
    }

    // 用strtok按"."分割,每一段前写一个长度字节
    char *token = strtok(hostname_dup, delim);
    while (token != NULL) {
        size_t len = strlen(token);
        *qname = (unsigned char)len;
        qname++;
        memcpy(qname, token, len);
        qname += len;

        token = strtok(NULL, delim);
    }

    *qname = 0;  // 最后补0,表示QNAME结束
    free(hostname_dup);

    return 0;
}

// =======================
// 构造完整DNS请求报文:header + question
// =======================
int dns_build_request(struct dns_header *header, struct dns_question *question, char *request, int rlen) {
    if (header == NULL || question == NULL || request == NULL) return -1;

    memset(request, 0, rlen);
    int offset = 0;

    // 拷贝DNS头部到请求buffer
    memcpy(request, header, sizeof(struct dns_header));
    offset += sizeof(struct dns_header);

    // 拷贝QNAME
    memcpy(request + offset, question->name, question->length);
    offset += question->length;

    // 拷贝QTYPE
    memcpy(request + offset, &question->qtype, sizeof(question->qtype));
    offset += sizeof(question->qtype);

    // 拷贝QCLASS
    memcpy(request + offset, &question->qclass, sizeof(question->qclass));
    offset += sizeof(question->qclass);

    return offset;  // 返回构建好的报文长度
}

// =======================
// 核心函数:发送DNS请求并接收响应
// =======================
int dns_client_commit(const char *domain) {
    // 创建UDP socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        return -1;
    }

    // 配置目标DNS服务器地址
    struct sockaddr_in servaddr = {0};
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(DNS_SERVER_PORT);
    servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP);

    // 可选:使用connect设置默认目标地址(UDP也可以)
    connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));

    // 构造DNS请求头
    struct dns_header header = {0};
    dns_create_header(&header);

    // 构造问题部分(域名)
    struct dns_question question = {0};
    dns_create_question(&question, domain);

    // 构造完整请求包
    char request[1024] = {0};
    int length = dns_build_request(&header, &question, request, sizeof(request));

    // 使用sendto发送数据
    int slen = sendto(sockfd, request, length, 0, (struct sockaddr*)&servaddr, sizeof(struct sockaddr_in));

    // 接收DNS响应
    char response[1024] = {0};
    struct sockaddr_in addr;
    size_t addr_len = sizeof(struct sockaddr_in);

    int n = recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr*)&addr, (socklen_t*)&addr_len);

    // 打印响应长度和部分内容(调试用)
    printf("recvfrom : %d, %s\n", n, response);

    return n;
}

// =======================
// 程序主入口
// =======================
int main(int argc, char *argv[]) {
    if (argc < 2) return -1;  // 没传域名参数,退出

    dns_client_commit(argv[1]);  // 调用主流程

    return 0;
}

https://github.com/0voice


网站公告

今日签到

点亮在社区的每一天
去签到