基于原始套接字(SOCK_RAW)实现TCP握手时连接重置且recvfrom阻塞问题
解决RAW套接字实现TCP握手时内核自动发送RST的问题
嘿,这个问题我太熟悉了——用SOCK_RAW手动搞TCP握手时,内核总是抢着发RST,简直像个爱管闲事的家伙!你看到的RST根本不是你的程序发的,是操作系统内核搞的鬼,原因很简单:
当服务器返回的SYN+ACK报文到达你的机器时,内核检查本地的TCP连接表,发现没有对应这个四元组(源IP/端口、目的IP/端口)的连接记录,它就会认为这是一个无效的报文,直接发送RST来终止这个“不存在”的连接。同时,内核可能已经把SYN+ACK报文拦截了,根本没传到你的recvfrom()调用里,这就是程序阻塞的原因。
下面给你几个具体的解决步骤,按顺序来:
1. 先阻止内核处理目标SYN+ACK报文
最直接的方法是用iptables规则,把服务器发过来的SYN+ACK报文拦截,不让内核看到:
# 替换成你的目标服务器IP和端口 iptables -A INPUT -p tcp -s 服务器IP --sport 服务器端口 --tcp-flags SYN,ACK SYN,ACK -j DROP
这样内核就不会触发自动RST的逻辑,报文会被送到你的RAW套接字里。
2. 正确配置RAW套接字选项
创建套接字时,一定要开启IP_HDRINCL选项,让你手动构造完整的IP头部,避免内核自动添加的IP头和你构造的TCP头不匹配:
#include <sys/socket.h> #include <netinet/in.h> #include <stdlib.h> #include <stdio.h> int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); if (sockfd < 0) { perror("socket creation failed"); exit(EXIT_FAILURE); } // 开启IP_HDRINCL,手动构造IP头 int opt = 1; if (setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &opt, sizeof(opt)) < 0) { perror("setsockopt IP_HDRINCL failed"); close(sockfd); exit(EXIT_FAILURE); }
3. 确保TCP报文的关键字段正确
手动构造TCP报文时,几个字段绝对不能错:
- 序列号(seq):你发送SYN时的seq可以随机生成,比如用
rand()或者时间戳 - 确认号(ack):收到服务器的SYN+ACK后,ack必须是服务器的seq + 1
- 标志位:SYN报文要把SYN位置1,SYN+ACK回复要把SYN和ACK位都置1,ACK报文把ACK位置1
- 校验和:TCP校验和必须包含伪头部(IP源地址、目的地址、协议号、TCP长度),这个是高频出错点!
给你一个实用的TCP校验和计算函数:
#include <stdint.h> #include <string.h> #include <netinet/in.h> uint16_t tcp_checksum(uint8_t *tcp_packet, int tcp_len, struct in_addr src_ip, struct in_addr dst_ip) { // 构造TCP伪头部 uint8_t pseudo_header[12]; memcpy(pseudo_header, &src_ip, 4); memcpy(pseudo_header + 4, &dst_ip, 4); pseudo_header[8] = 0; pseudo_header[9] = IPPROTO_TCP; uint16_t tcp_len_net = htons(tcp_len); memcpy(pseudo_header + 10, &tcp_len_net, 2); // 合并伪头部和TCP报文,计算校验和 uint32_t sum = 0; int i; // 计算伪头部 for (i = 0; i < 12; i += 2) { sum += ((uint16_t)pseudo_header[i] << 8) | pseudo_header[i + 1]; } // 计算TCP报文 for (i = 0; i < tcp_len; i += 2) { if (i + 1 < tcp_len) { sum += ((uint16_t)tcp_packet[i] << 8) | tcp_packet[i + 1]; } else { sum += (uint16_t)tcp_packet[i] << 8; } } // 折叠校验和 while (sum >> 16) { sum = (sum & 0xFFFF) + (sum >> 16); } return ~sum; }
4. 确保recvfrom能捕获到报文
RAW套接字需要root权限运行,另外你可以在recvfrom()时过滤来源地址,只处理目标服务器的报文:
#define BUFFER_SIZE 4096 uint8_t buffer[BUFFER_SIZE]; struct sockaddr_in src_addr; socklen_t addr_len = sizeof(src_addr); ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&src_addr, &addr_len); if (recv_len > 0) { // 检查来源是否是目标服务器 if (src_addr.sin_addr.s_addr == inet_addr("服务器IP") && src_addr.sin_port == htons(服务器端口)) { // 处理SYN+ACK报文的逻辑写在这里 printf("Received SYN+ACK from server\n"); } }
按照这几步来,你应该就能解决内核发RST和recvfrom()阻塞的问题了。
内容的提问来源于stack exchange,提问作者machacamoya
相关产品推荐
相关产品推荐

