无状态TCP技术:DNS代理的轻量级实现逻辑与核心原理(C/C++代码实现)

日常上网离不开DNS(域名系统),但很少有人注意到:DNS默认用UDP协议传输,可一旦查询结果超过512字节(比如包含大量IP的CDN域名),客户端就会自动切换到TCP 53端口重新请求。传统的TCP DNS服务需要维护每一个连接的状态(比如三次握手、四次挥手的上下文),高并发时会消耗大量服务器资源;而"无状态TCP"技术能绕开连接维护,用更轻量的方式处理TCP DNS请求,甚至能实现DNS请求的转发或劫持------这就是我们今天要拆解的核心技术思路。

一、先搞懂两个核心概念:有状态VS无状态TCP

要理解这个方案,先分清"有状态TCP"和"无状态TCP"的区别,用大白话讲就是:

  • 有状态TCP:像我们和朋友打电话,从拨号(三次握手)到挂电话(四次挥手),全程要记住"通话状态"------比如对方说了什么、当前聊到哪一步。服务器会记录每个连接的SEQ/ACK序号、窗口大小、连接阶段,优点是可靠,但高并发时会占满内存和CPU。
  • 无状态TCP:像街边发传单,不管谁来拿,发完就忘,不记任何人的信息。服务器收到每个TCP报文后,只根据当前报文的内容(比如源IP、端口、SEQ序号)生成响应,不用记住之前的交互,优点是极致轻量化,缺点是需要手动"伪造"TCP报文,对协议细节要求极高。

另外要补充:TCP DNS和UDP DNS有个关键区别------TCP DNS的报文开头会加2个字节的"长度字段"(告诉对方后面的DNS数据有多长),而UDP DNS没有这个字段;无状态方案的核心之一,就是处理好这个长度字段的"加/减"。

二、无状态TCP实现DNS代理的核心原理

整个流程就像"手工模拟TCP服务器",全程绕开操作系统的有状态TCP协议栈,手动处理每一个数据包,拆解成5个关键步骤:

1. 网卡抓包:只盯TCP 53端口的报文

首先用libpcap库(tcpdump、Wireshark的底层核心)监听指定网卡的所有数据包,通过过滤规则只保留"目的端口是53的TCP报文"------相当于"守在网卡门口",只捡和TCP DNS相关的数据包,其他报文直接忽略。

2. 解析报文:剥洋葱式拆出核心数据

拿到数据包后,像剥洋葱一样逐层解析:

  • 先剥14字节的以太网头(不管,只取后面的IP报文);
  • 再解析IP头:获取源IP、目的IP、IP头长度、协议类型(确认是TCP);
  • 最后解析TCP头:获取源端口、目的端口、SEQ/ACK序号、报文标志位(SYN/FIN/ACK)、TCP头长度,以及后面的DNS数据。

这里要注意:IP头和TCP头的长度都不是固定的(因为有可选字段),必须用协议宏(比如IP_HL、TH_OFF)计算实际长度,否则会解析错数据位置。

3. 无状态响应:伪造TCP报文"骗"客户端

客户端发TCP报文时,会带不同的标志位,服务器要针对性伪造响应,全程不记录任何状态:

  • 收到SYN报文(客户端想建立连接):直接伪造SYN+ACK报文发回去。关键是交换源/目的IP和端口,SEQ序号用当前时间生成(不用记),ACK序号是客户端SEQ+1,还要加MSS(最大分段大小)选项(告诉客户端单次能传1220字节),最后算好IP和TCP校验和。
  • 收到FIN报文(客户端想关闭连接):伪造ACK报文回应,同样交换地址/端口,计算正确的SEQ/ACK,发完就忘。
  • 收到带数据的报文(DNS查询):先伪造一个ACK报文回应(防止客户端超时重传),再处理后面的DNS数据。

4. 转发请求:把TCP DNS转成UDP发给后端

TCP DNS报文开头有2字节长度字段,UDP DNS不需要,所以先剥掉这2字节,然后通过普通UDP Socket发给后端的真实DNS服务器(比如8.8.8.8)------这么做是因为公共DNS的UDP端口更稳定、处理更快,还不用和后端建立TCP连接,进一步简化逻辑。

5. 封装响应:把UDP结果转回TCP发给客户端

收到后端DNS的UDP响应后,先加回2字节的长度字段(还原成TCP DNS格式),如果响应太大,就分成512字节的小块(符合TCP的MSS限制),为每个小块伪造TCP报文(带ACK标志),最后一块还要加FIN标志(表示数据发完),计算好校验和后发送给客户端。

三、设计思路:为什么要这么做?

这个方案的设计核心是"极致轻量化",所有逻辑都围绕"不维护连接状态"展开:

  1. 不用连接表,省资源:传统TCP服务器要为每个连接存SEQ/ACK、窗口大小等信息,无状态方案只处理当前报文,哪怕十万个客户端同时请求,内存占用也不会暴涨,适合嵌入式设备、低配置服务器。
  2. Raw Socket绕开系统协议栈:普通Socket(TCP/UDP)由操作系统处理协议头,而Raw Socket能让程序直接操作IP/TCP头,手动伪造所有字段------这是实现无状态的关键,相当于"跳过操作系统,自己做TCP协议栈"。
  3. UDP转发简化逻辑:后端用UDP而非TCP,不用和后端维护连接,进一步降低复杂度;只需要处理"TCP转UDP""UDP转TCP"的格式转换(核心是2字节长度字段)。
  4. 分片发送防丢包:把大响应分成512字节块,符合TCP的最大分段大小,避免报文被IP分片,提高传输可靠性。

四、涉及的核心技术领域(知识点总结)

这个方案看似是"DNS代理",实则覆盖了网络编程的多个核心领域,是学习底层协议的绝佳案例:

1. libpcap抓包技术

所有抓包工具的底层核心,能直接从网卡获取原始数据包,支持自定义过滤规则(比如只抓TCP 53端口),是实现"报文监听"的基础。

2. Raw Socket编程

网络编程的高级用法,普通Socket只处理应用层数据,Raw Socket能直接操作IP/TCP层,需要手动构造协议头、计算校验和------适合做协议分析、报文伪造、网络测试。

3. TCP/IP协议栈细节

  • IP头:要关注版本+头长度、总长度、DF标志(不分片)、TTL、协议类型(6=TCP)、校验和;
  • TCP头:核心是SEQ/ACK序号(无状态下用时间生成SEQ,ACK是客户端SEQ+1)、标志位(SYN/FIN/ACK)、MSS选项、校验和;
  • TCP伪头:计算TCP校验和必须包含伪头(IP源/目的地址、协议号、TCP长度),否则报文会被客户端丢弃。

4. DNS协议格式

关键是区分TCP和UDP的差异:TCP DNS开头加2字节长度字段,UDP没有;DNS查询/响应的核心格式一致,只是标志位不同。

5. 校验和计算

IP和TCP校验和都用"反码求和":先把数据按16位分组求和,再对结果取反;计算时要先把校验和字段设为0,最后填回结果。

五、实现流程示意图

SYN(建立连接)
FIN(关闭连接)
有数据(DNS查询)
libpcap监听网卡
过滤出TCP 53端口报文
解析以太网/IP/TCP头
判断TCP标志位
伪造SYN+ACK报文发送
伪造ACK报文发送
伪造ACK报文回应(防重传)
剥掉TCP DNS的2字节长度字段
通过UDP转发给后端DNS
接收后端UDP DNS响应
添加2字节长度字段,分512字节块
为每个块伪造TCP报文(ACK标志)
最后一块添加FIN标志,发送
结束单次报文处理(无状态,不记录)

cpp 复制代码
...
struct ip_hdr {
  u_char  ip_vhl;                           
#define IP_HL(ip)    (((ip)->ip_vhl) & 0x0f)
#define IP_V(ip)     (((ip)->ip_vhl) >> 4)
  u_char  ip_tos;                            
  u_short ip_len;                           
  u_short ip_id;                            
  u_short ip_off;                            
#define IP_RF 0x8000                       
#define IP_DF 0x4000                        
#define IP_MF 0x2000                         
#define IP_OFFMASK 0x1fff                    
  u_char  ip_ttl;                        
  u_char  ip_p;                            
  u_short ip_sum;                           
  struct  in_addr ip_src,ip_dst;            
  };



typedef u_int32_t tcp_seq ;

struct tcp_hdr {
  u_short th_sport;                         
  u_short th_dport;                         
  tcp_seq th_seq;                          
  tcp_seq th_ack;                           
  u_char  th_offx2;                          
#define TH_OFF(th)      (((th)->th_offx2 & 0xf0) >> 4)
  u_char  th_flags;
#define TH_FIN  0x01
#define TH_SYN  0x02
#define TH_RST  0x04
#define TH_PUSH 0x08
#define TH_ACK  0x10
#define TH_URG  0x20
#define TH_ECE  0x40
#define TH_CWR  0x80
#define TH_FLGS        (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR)
  u_short th_win;                         
  u_short th_sum;                            
  u_short th_urg;                            
  };

struct tcp_option {
  u_char opt_type ;
  u_char opt_len ;
  u_short opt_val ;
  } ;

...
void got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet)
{
...
	
	
  ip = (struct ip_hdr*)(packet + SIZE_ETHERNET);
  size_ip = IP_HL(ip)*4;
  if (size_ip < 20) {
    printf("   * Invalid IP header length: %u bytes\n", size_ip);
    return;
    }
  if (ip->ip_p != IPPROTO_TCP) 
    return ;	
	
  tcp = (struct tcp_hdr*)(packet + SIZE_ETHERNET + size_ip);
  size_tcp = TH_OFF(tcp)*4;
  if (size_tcp < 20) {
    printf("   * Invalid TCP header length: %u bytes\n", size_tcp);
    return;
    }

  if (ntohs(tcp->th_dport) != PORT)
    return ;


  ip_payload = (u_char *)(packet + SIZE_ETHERNET);
  payload = (u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);
	
  size_payload = ntohs(ip->ip_len) - (size_ip + size_tcp);

  if (tcp->th_flags & TH_SYN) 
  {
    send_tcp_synack(ip_payload) ;
    return ;
    }

  if (tcp->th_flags & TH_FIN) 
  {
    send_tcp_ack(ip_payload) ;
    return ;
    }

  if (size_payload > 0) 
  {
    bcopy(payload,cmd_buffer,size_payload) ;
    cmd_buffer[size_payload] = '\0';
    server_request(ip_payload,cmd_buffer,size_payload) ;
    }
  return;
  }

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

...


  sprintf(filter_exp,"dst port %d and dst host %s",PORT,THIS_HOST) ;

  

  if (argc == 2) 
  {
    dev = argv[1];
  }
  else if (argc > 2) 
  {
    fprintf(stderr, "error: unrecognized command-line options\n\n");
    exit(EXIT_FAILURE);
  }
  else
  {

    dev = pcap_lookupdev(errbuff);
    if (dev == NULL) 
	    {
	      fprintf(stderr, "Couldn't find default device: %s\n",errbuff);
	      exit(EXIT_FAILURE);
	    }
  }
	
  if (pcap_lookupnet(dev, &net, &mask, errbuff) == -1) 
  {
    fprintf(stderr, "Couldn't get netmask for device %s: %s\n", dev, errbuff);
    net = 0;
    mask = 0;
   }

  in = (struct in_addr *) &net ;
  printf("Device: %s Network: %s Mask: %x\n", dev,inet_ntoa(*in),ntohl(mask));
  printf("Filter expression: %s\n", filter_exp);

  handle = pcap_open_live(dev, SNAP_LEN, 1, 1000, errbuff);	 
  if (handle == NULL) 
  {		 
    fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuff);
    exit(EXIT_FAILURE) ;
   }	 


  if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) 
  {		 
    fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
    exit(EXIT_FAILURE) ;
  }	 

  if (pcap_setfilter(handle, &fp) == -1) 
  {		 
    fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
    exit(EXIT_FAILURE) ;
  }

  open_raw_socket() ;
  pcap_loop(handle, -1, got_packet, NULL) ;

  pcap_close(handle);
  return(0);
}

If you need the complete source code, please add the WeChat number (c17865354792)

执行以下命令启动程序,指定要监听的网卡(比如eth0,根据你的机器调整):

bash 复制代码
sudo ./test_dns eth0

此时程序会一直运行,监听eth0网卡上发往本机TCP 53端口的所有报文。

六、测试验证(核心步骤)

我们需要验证两个核心点:①无状态TCP的SYN+ACK响应是否正常 ②DNS查询是否能通过这个程序转发并返回结果。

测试1:验证TCP 53端口的无状态响应

nc(netcat)工具模拟TCP DNS客户端,发起连接请求:

bash 复制代码
# 新开一个终端,执行以下命令(替换成你的测试机IP)
nc -v 192.168.1.100 53

如果程序正常工作,nc会显示:

复制代码
Connection to 192.168.1.100 53 port [tcp/domain] succeeded!

这说明程序伪造的SYN+ACK报文生效了------因为系统本身没有监听TCP 53端口,是程序手动返回的SYN+ACK,模拟了TCP连接建立。

测试2:验证DNS查询转发功能

dig命令(DNS查询工具)指定TCP协议,向测试机发起DNS查询:

bash 复制代码
# 关键参数:@测试机IP 域名 tcp(强制用TCP查询)
dig @192.168.1.100 www.baidu.com tcp

如果程序正常工作,dig会返回百度的DNS解析结果,和直接查询8.8.8.8的结果一致;如果返回超时,说明转发环节有问题(比如后端DNS不可达、代码里的长度字段处理错误)。

测试3:抓包验证(进阶,看底层报文)

tcpdump抓包,直观看到无状态TCP的报文交互:

bash 复制代码
# 新开终端,抓eth0网卡上的TCP 53端口报文
sudo tcpdump -i eth0 -nn port 53 and tcp

然后再执行dig @测试机IP www.baidu.com tcp,你会看到:

  1. 客户端发SYN报文 → 程序返回SYN+ACK(伪造的);
  2. 客户端发带DNS查询的TCP报文 → 程序返回ACK(伪造的);
  3. 程序向8.8.8.8发UDP DNS查询 → 8.8.8.8返回UDP响应;
  4. 程序把UDP响应封装成TCP报文(加2字节长度字段)发给客户端;
  5. 最后程序发FIN+ACK关闭连接(无状态,不用等客户端的FIN)。

总结

无状态TCP实现DNS代理的核心,是"手动替代操作系统的TCP协议栈"------从抓包、解析、伪造响应,到转发请求、封装响应,全程不保存任何连接状态。这种方案的优势是极致轻量化,适合资源受限的场景;但缺点也很明显,需要对TCP/IP协议细节了如指掌,且不支持TCP的高级特性(比如窗口缩放),属于"极简版"TCP实现。

从学习角度看,这个方案把抓包、原始套接字、协议解析、报文伪造等知识点串联起来,能帮我们跳出"调用API"的表层,深入理解TCP/IP协议的本质------毕竟,无状态TCP的实现,就是把操作系统帮我们做的事,手动重新做了一遍。

Welcome to follow WeChat official account【程序猿编码

相关推荐
小二·2 小时前
Python Web 开发进阶实战:可验证网络 —— 在 Flask + Vue 中实现去中心化身份(DID)与零知识证明(ZKP)认证
前端·网络·python
2401_865854882 小时前
腾讯云的IP是原生IP吗?
tcp/ip·云计算·腾讯云
Vallelonga2 小时前
Rust 中 extern “C“ 关键字
c语言·开发语言·rust
饿了么骑手贪大心2 小时前
简单易用的网络测试工具——Clumsy使用总结
网络·测试工具
期货资管源码2 小时前
智星期货资管子账户软件pc端开发技术栈的选择
c语言·数据结构·c++·vue
Jia ming2 小时前
游戏卡顿?SMB传输惹的祸!
网络
头发还没掉光光2 小时前
Linux网络之TCP协议
linux·运维·开发语言·网络·网络协议·tcp/ip
尼古拉斯·纯情暖男·天真·阿玮2 小时前
实验七 防火墙与入侵防护实验
linux·服务器·网络
chenzhiyuan20182 小时前
ARMxy+Node-RED+FUXA:一台设备实现采集、控制与可视化
网络