C语言手写一个简易 DNS 客户端

本文聚焦讲解如何通过 C 语言构造并发送一个最小化的 DNS 请求,特别以 dns_client_commit() 函数为主线,带你一步步理解 DNS 请求的构造过程。

为什么要学习 DNS 报文构造?

我们平时在浏览器里输入一个网址(比如 www.baidu.com),浏览器其实背后会通过操作系统的 DNS 模块发送一个查询请求,将域名解析为 IP 地址。

而如果我们手动用 C 语言自己构造 DNS 请求,我们可以更深刻地理解底层网络通信的细节,比如 UDP 套接字、报文格式、DNS 协议结构等。

第一步:创建 UDP 套接字

cpp 复制代码
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
    return -1;
}
  • socket(AF_INET, SOCK_DGRAM, 0):创建一个基于 IPv4 的 UDP 套接字。

  • SOCK_DGRAM 指的是数据报套接字(即 UDP)。

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

UDP 是 DNS 最常用的传输方式,简单快速,适合小数据量通信。

第二步:配置服务器地址结构

cpp 复制代码
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);
socklen_t addr_len = sizeof(servaddr);
  • sin_family 设置为 AF_INET 表示 IPv4;

  • sin_port 使用 htons 转换为网络字节序的端口(这里是 53);

  • sin_addr.s_addr 是将字符串 IP 地址 "114.114.114.114" 转为 32 位网络地址。

使用 inet_addr() 转换字符串 IP 为二进制,便于系统识别。

第三步:创建 DNS 报文头部结构

cpp 复制代码
struct dns_header header = {0};
dns_create_header(&header);
  • 报文头结构体填入了如下字段:

    • 随机 ID:标记请求和响应是否匹配;

    • 标志位:设置为 0x0100(标准查询,递归);

    • 查询数量设为 1。

所有字段都转为 网络字节序 (用 htons())是网络编程常识。

第四步:构造域名查询部分(Question 区)

cpp 复制代码
struct dns_question question = {0};
dns_create_question(&question, domain);

将传入的 domain 字符串,如 www.baidu.com,转换为 DNS 格式的 QNAME:

cpp 复制代码
03 77 77 77 05 62 61 69 64 75 03 63 6f 6d 00
  • 每段前面加一个长度字节,末尾以 0x00 结尾。

  • 同时设置 QTYPE=1(A记录)、QCLASS=1(IN 类)。

第五步:构造完整 DNS 请求报文

cpp 复制代码
char request[1024] = {0};
int length = dns_build_requestion(&header, &question, request);
  • 该函数负责将 header + question 拼接到请求缓冲区中;

  • 返回实际请求报文的长度(单位:字节);

  • request 就是我们最终要发出去的数据。

第六步:发送 UDP 请求

cpp 复制代码
sendto(sockfd, request, length, 0, (struct sockaddr*)&servaddr, addr_len);
  • 使用 sendto() 直接把 DNS 报文发往目标服务器;

  • 无需建立连接;

  • 这是 UDP 的典型使用方式。

小结:dns_client_commit 的完整流程图

cpp 复制代码
┌─────────────┐
│ 输入域名    │
└────┬────────┘
     ↓
┌──────────────────────┐
│ 创建 UDP socket      │
└────┬─────────────────┘
     ↓
┌──────────────────────┐
│ 填充服务器地址结构   │
└────┬─────────────────┘
     ↓
┌──────────────────────┐
│ 构造 DNS Header      │
└────┬─────────────────┘
     ↓
┌──────────────────────┐
│ 构造 DNS Question     │
└────┬─────────────────┘
     ↓
┌──────────────────────┐
│ 拼接完整报文         │
└────┬─────────────────┘
     ↓
┌──────────────────────┐
│ 发送 UDP 请求         │
└────┬─────────────────┘
     ↓
┌──────────────────────┐
│ 关闭 socket           │
└──────────────────────┘

完整代码

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define DNS_SERVER_PORT     53  
#define DNS_SERVER_IP       "114.114.114.114"

struct dns_header{
    unsigned short id;
    unsigned short flags;
    unsigned short questions;
    unsigned short answers;
    unsigned short authority;
    unsigned short additional;
};

struct dns_question{
    int length;
    unsigned short qtype;
    unsigned short qclass;
    unsigned char *name;
};

int dns_create_header(struct dns_header *header){
    if(header == NULL) return -1;
    memset(header,0,sizeof(struct dns_header));
    srand((unsigned int)time(NULL));
    header->id = (unsigned short)rand();
    header->flags = htons(0x0100);
    header->questions = htons(1);
    return 0;
}

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);
    if(question->name == NULL) return -2;
    question->length = (int)hostlen + 2;
    question->qtype = htons(1);
    question->qclass = htons(1);
    //name
    const char delim[2] = ".";
    unsigned char *qname = question->name;
    char *hostname_dup = strdup(hostname);
    if(hostname_dup == NULL) {
        free(question->name);
        return -3;
    }
    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; // QNAME 结尾补0
    free(hostname_dup);
    return 0;
}

int dns_client_commit(const char *domain){

    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if (sockfd < 0)
    {
        return -1;
    }
    
    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);
    socklen_t addr_len = sizeof(servaddr); 

    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_requestion(&header,&question,request);

    // request
    sendto(sockfd,request,length,0,(struct sockaddr*)&servaddr,addr_len);
    close(sockfd);
}

https://github.com/0voice

相关推荐
上海广测检测科技有限公司23 分钟前
消费类智能联网设备安全分级国标征求意见要点解析:三级五维框架与跨境对标逻辑
网络·经验分享
别动我齐刘海23 分钟前
Linux 系统调试学习1
linux·运维·服务器·c++·学习·机器学习·机器人
Lsetea36 分钟前
OpenSSL报certificate chain too long:error 22与verify_depth深度排查
linux·https·ssl证书·openssl·证书链
QZ166560951591 小时前
金融数据流转监测排行:从看见到拦截的距离
网络·安全·网络安全·金融
不像程序员的程序媛2 小时前
linux常见文本操作命令
linux·运维·服务器
wangbing11252 小时前
开发指南149-IotDB-常用命令
linux·运维·iotdb
看着博客敲代码2 小时前
WorkBuddy 接上 Kali MCP 做自动化渗透:信息收集到初步验证的落地记录
linux·人工智能·安全·网络安全·自动化
蜗牛互联网3 小时前
Python Agent工具调用的白名单、参数校验与审批门禁
java·服务器·前端·人工智能·后端·python
zxanz14 小时前
SSL 证书申请下来快吗?
网络·网络协议·ssl
false_or_true4 小时前
一次由 tcp_tw_recycle 引发的“灵异”故障排查:基于双网闸架构的踩坑记
网络·tcp/ip·架构