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

相关推荐
YC运维15 分钟前
网络配置综合实验全攻略(对之前学习的总结)
linux·服务器·网络
平凡灵感码头1 小时前
什么是 Bootloader?怎么把它移植到 STM32 上?
linux·soc
Xi-Xu1 小时前
隆重介绍 Xget for Chrome:您的终极下载加速器
前端·网络·chrome·经验分享·github
无敌的牛1 小时前
Linux基础开发工具
linux·运维·服务器
Edingbrugh.南空1 小时前
实战指南:用pmap+gdb排查Linux进程内存问题
linux·运维·服务器
亚马逊云开发者2 小时前
将 Go 应用从 x86 平台迁移至 Amazon Graviton:场景剖析与最佳实践
linux·数据库·golang
大叔是90后大叔2 小时前
Linux/Ubuntu安装go
linux·ubuntu·golang
孙克旭_2 小时前
day051-ansible循环、判断与jinja2模板
linux·运维·服务器·网络·ansible
渡我白衣3 小时前
Linux操作系统之进程间通信:共享内存
linux
总有刁民想爱朕ha3 小时前
零基础搭建监控系统:Grafana+InfluxDB 保姆级教程,5分钟可视化服务器性能!
运维·服务器·grafana