1. 这不是教科书里的“网络协议”,而是你亲手拆开网线后看到的真实字节流
如果你在某次调试中抓到一个发往192.168.1.105的UDP包,源端口54321,目的端口6666,Payload里赫然写着“飞鸽传书v2.3_在线”,但你根本没装过这个软件——恭喜,你已经站在了原始套接字(Raw Socket)世界的门口。这不是抽象的OSI七层模型图,也不是Wireshark里点几下就能高亮的“TCP Stream”;这是你绕过操作系统内核默认协议栈、直接与网卡驱动对话的权限,是能手动构造MAC帧头、IP首部、TCP选项字段、甚至伪造SYN+ACK序列号的底层能力。标题里提到的“飞鸽欺骗”和“三次握手连接器”,本质上都是这种能力的具象化应用:前者靠UDP无连接特性批量伪造源IP+端口,向局域网广播“我在线”,后者则用TCP原始套接字逐字节拼出SYN包、解析返回的SYN-ACK、再发出最终ACK,完成一次不经过connect()系统调用的“裸连”。我带过的几个某高校网络攻防实训班学员,第一次用C语言调用socket(AF_INET, SOCK_RAW, IPPROTO_TCP)成功发出自定义TCP标志位包时,盯着tcpdump输出里那个纯手工打出来的SYN包愣了两分钟——因为那意味着,他们终于把课本上画了十年的三次握手箭头,真正捏在了自己手里。这篇文章不讲RFC文档编号,不列枯燥的字段偏移量,只说清三件事:为什么非得用原始套接字才能干这些事;每个字节你到底要填什么、为什么这么填;以及,当你真把包发出去之后,网络设备和对端主机究竟会怎么“看”你这个包。适合正在啃《TCP/IP详解》卷一却总卡在“校验和怎么算”那页的进阶学习者,也适合想搞懂内网渗透工具底层原理的红队新手——毕竟,所有看似魔法的“自动发现”“无感知连接”,拆开外壳全是这几行C代码和几个十六进制数。
2. 原始套接字为何是唯一入口:绕过内核协议栈的底层逻辑与硬性限制
2.1 普通套接字 vs 原始套接字:操作系统划下的那条“权限分界线”
普通socket(AF_INET, SOCK_STREAM, 0)创建的是传输层抽象接口,它把开发者完全隔绝在内核协议栈之外。当你调用send()发送数据时,内核会自动完成:IP首部填充(源/目的IP)、TCP首部组装(序列号、确认号、标志位、窗口大小)、TCP校验和计算、MSS协商、重传定时器启动……整个过程像坐电梯——你按楼层(调用send),但不知道钢缆怎么拉、制动器何时咬合。而原始套接字socket(AF_INET, SOCK_RAW, IPPROTO_TCP)则是直接拿到电梯井道的检修口钥匙:你得自己焊钢缆、调制动间隙、甚至给每块配重铁标上克数。关键区别在于协议栈处理阶段的介入点不同:
- 普通套接字:数据从应用层进入,经内核TCP模块→IP模块→链路层驱动,全程由内核接管;
- 原始套接字:你提交的数据包(含完整IP首部+TCP首部+载荷)被内核仅做链路层封装(加MAC头、CRC校验),然后直送网卡驱动;IP层及以下的校验、分片、路由等逻辑,要么由你手动实现,要么被内核跳过。
提示:Linux下创建原始套接字需root权限(或CAP_NET_RAW能力),Windows需管理员提权。这不是系统故意刁难,而是安全红线——允许普通用户随意构造IP头,等于开放伪造源地址、发起IP分片攻击、绕过防火墙规则的通道。
2.2 为什么“飞鸽欺骗”必须用UDP原始套接字?
市面上所谓“飞鸽传书”类局域网通信工具,其核心发现机制本质是UDP广播探测。标准实现中,客户端启动后向255.255.255.255:2425端口发送UDP包,内容为“HELLO|VERSION=2.3|USER=张三”,所有监听该端口的服务端收到后回复“ALIVE|IP=192.168.1.105|PORT=2425”。但问题来了:如果服务端防火墙禁止了入站UDP 2425端口,或者客户端本身处于NAT后无法接收广播响应,传统方式就失效了。此时“欺骗”方案登场——不用等对方响应,而是主动伪造大量源IP为“192.168.1.x”的UDP包,向全网段广播“我在线”消息。这要求你:
- 能指定任意源IP地址(普通UDP socket受内核限制,源IP只能是本机真实IP);
- 能控制UDP包的TTL值(设为1确保不离开本子网);
- 能绕过内核对UDP校验和的强制计算(某些场景需设为0表示校验和禁用)。
只有原始套接字能同时满足这三点。实测中,我们曾用Python的scapy库构造如下UDP包:
from scapy.all import * ip = IP(src="192.168.1.200", dst="255.255.255.255", ttl=1) udp = UDP(sport=54321, dport=2425, chksum=0) # chksum=0表示禁用校验 payload = b"HELLO|VERSION=2.3|USER=李四" send(ip/udp/payload, iface="eth0", verbose=False)当这个包被发往交换机时,所有在线主机的ARP表都会更新“192.168.1.200对应MAC地址XX:XX:XX:XX:XX:XX”,后续发往该IP的流量自然被导向伪造者——这就是“飞鸽欺骗”的物理层基础。没有原始套接字,你连伪造源IP这第一步都迈不出去。
2.3 为什么“三次握手连接器”必须用TCP原始套接字?
TCP连接建立的三次握手(SYN→SYN-ACK→ACK)看似简单,但普通socket的connect()调用会触发内核全套状态机:生成初始序列号(ISN)、设置重传定时器、维护连接队列……而“三次握手连接器”的目标恰恰是绕过内核状态管理,实现无连接态的单次握手探测。典型应用场景包括:
- 对防火墙策略的精准测绘:发送SYN包,若收到RST则说明端口关闭;若超时无响应,可能被防火墙丢弃;
- TCP端口扫描优化:避免因connect()阻塞导致扫描速度下降;
- 自定义TCP选项注入:如在SYN包中加入MSS、SACK、时间戳等选项,测试目标主机协议栈兼容性。
要达成此目标,你必须能:
- 手动指定TCP首部所有字段(尤其是序列号、确认号、标志位);
- 精确控制IP首部的ID、DF、MF、分片偏移等字段(影响分片行为);
- 计算并填入正确的TCP校验和(涉及伪首部+TCP首部+载荷)。
普通socket无法暴露TCP首部控制权,而原始套接字让你直接操作字节流。我们曾用C语言实现一个最小化SYN发送器,核心逻辑是:
// 构造IP首部(20字节) iph->ihl = 5; // 首部长度5*4=20字节 iph->version = 4; // IPv4 iph->tos = 0; // 服务类型 iph->tot_len = htons(40); // 总长=IP头20+TCP头20 iph->id = htons(54321); // 标识符(可自定义) iph->frag_off = 0; // 不分片 iph->ttl = 64; // TTL iph->protocol = IPPROTO_TCP; iph->check = csum((unsigned short*)buf, 20); // IP校验和 // 构造TCP首部(20字节) tcph->source = htons(12345); // 源端口 tcph->dest = htons(80); // 目的端口 tcph->seq = htonl(11000); // 初始序列号 tcph->ack_seq = 0; // SYN包ack_seq为0 tcph->doff = 5; // 数据偏移5*4=20字节 tcph->fin=0; tcph->syn=1; tcph->rst=0; tcph->psh=0; tcph->ack=0; tcph->urg=0; // 仅SYN置1 tcph->window = htons(65535); // 接收窗口 tcph->check = tcp_csum(iph, tcph); // 关键!需用伪首部计算其中tcp_csum()函数必须实现RFC 793规定的伪首部校验和算法,否则目标主机网卡驱动会在校验阶段直接丢弃该包。这个过程没有任何API帮你兜底,错一个字节,包就石沉大海。
3. 核心数据包结构拆解:从MAC帧到TCP载荷的逐层手算实践
3.1 MAC帧:链路层的“信封”,决定数据能否抵达正确收件人
所有网络通信始于MAC帧,它是数据在物理链路上传输的最小单元。以以太网II帧为例,其结构为:
| 字段 | 长度 | 说明 |
|---|---|---|
| 目的MAC地址 | 6字节 | 接收方网卡的物理地址,广播时为FF:FF:FF:FF:FF:FF |
| 源MAC地址 | 6字节 | 发送方网卡的物理地址,原始套接字中可任意伪造 |
| 类型字段 | 2字节 | 标识上层协议,0x0800=IPv4,0x0806=ARP,0x86DD=IPv6 |
关键细节与实操陷阱:
- 目的MAC地址的获取:若目标IP在同一子网,需先通过ARP协议查询其MAC。原始套接字不自动处理ARP,因此你必须:
- 调用
arping命令或发送ARP请求包; - 解析返回的ARP应答包,提取目标MAC;
- 将该MAC填入以太网帧头。 我们曾因忘记这一步,直接向错误MAC发包,结果包在交换机层面就被丢弃,tcpdump在本机抓不到任何痕迹,排查耗时3小时。
- 调用
- 源MAC地址伪造:Linux下可通过
ifconfig eth0 hw ether XX:XX:XX:XX:XX:XX临时修改,但更稳妥的方式是在原始套接字发送前,用libpcap或AF_PACKET socket直接构造完整以太网帧。注意:某些网卡驱动会校验源MAC合法性,伪造后可能触发驱动告警。 - 类型字段的致命性:若将IPv4包的类型字段误填为0x0806(ARP),交换机会将其转发给所有主机,但各主机ARP模块收到后会直接丢弃(因IP头不匹配),导致通信静默失败——这种错误在十六进制编辑器里肉眼极难发现。
3.2 IP首部:网络层的“邮编”,控制数据跨网段路由
IPv4首部(20字节固定部分)是原始套接字操作的核心战场,其字段含义与填写逻辑直接决定包能否被正确路由:
| 字段 | 位置 | 计算/填写要点 | 实操案例 |
|---|---|---|---|
| 版本+首部长度(IHL) | 0-3位+4-7位 | 版本=4,IHL=5(20字节) | iph->ihl = 5; iph->version = 4; |
| 服务类型(TOS) | 8-15位 | 通常设0,低延迟业务可设0x10(优先级) | 渗透测试中设0x00避免QoS策略拦截 |
| 总长度(Tot Len) | 16-31位 | htons(IP头20 + TCP头20 + 载荷长度) | 载荷100字节→总长140→htons(140) |
| 标识(ID) | 32-47位 | 每个IP包唯一,分片时相同ID标识同组 | 扫描工具常设固定ID便于追踪 |
| 标志(Flags)+分片偏移(Frag Off) | 48-63位 | DF=1(不分片),MF=0,偏移=0 | iph->frag_off = htons(0x4000)(DF置1) |
| 生存时间(TTL) | 64-71位 | 每经一跳减1,64/128为常见值 | 局域网探测设TTL=1,广域网设64 |
| 协议(Protocol) | 72-79位 | TCP=6,UDP=17,ICMP=1 | iph->protocol = IPPROTO_TCP; |
| 首部校验和(Header Checksum) | 80-95位 | 必须计算,否则路由器丢弃 | 调用csum((u16*)iph, 20) |
校验和计算原理与避坑: IP首部校验和采用反码求和算法:将首部每16位视为无符号整数相加,溢出位回卷(carry around),最后取反。关键点在于:
- 必须将校验和字段自身置0再计算,否则结果错误;
- 字节序转换:x86为小端,网络字节序为大端,需用
htons()转换; - 奇数长度处理:若首部长度为奇数,末尾补0字节参与计算(但不写入包中)。
我们曾因未将校验和字段清零,导致所有自定义IP包被路由器静默丢弃。用Wireshark抓包发现“Bad checksum”红色警告,但包内容完全正确——根源就在这一行缺失的iph->check = 0;。
3.3 TCP首部:传输层的“契约”,定义连接状态与数据边界
TCP首部(20字节基础+可选选项)是三次握手连接器的灵魂。其字段设计精妙,每个比特都有明确语义:
| 字段 | 位置 | 关键作用 | 填写规范 |
|---|---|---|---|
| 源/目的端口 | 0-15位/16-31位 | 标识应用进程 | htons(12345)/htons(80) |
| 序列号(Seq) | 32-63位 | 当前包第一个字节的序号 | SYN包中为随机ISN(如arc4random()) |
| 确认号(Ack Seq) | 64-95位 | 期望收到的下一个字节序号 | SYN包中为0;SYN-ACK中为ISN+1 |
| 数据偏移(Doff) | 96-99位 | TCP首部长度(单位:4字节) | 无选项时为5(20字节),有选项时递增 |
| 标志位(Control Bits) | 100-105位 | URG, ACK, PSH, RST, SYN, FIN | SYN包仅SYN=1,其余为0 |
| 窗口大小(Window) | 106-121位 | 接收方通告的可用缓冲区大小 | 通常设65535(最大值) |
| 校验和(Checksum) | 122-137位 | 必须计算,含伪首部+TCP首部+载荷 | 错误则包被丢弃 |
| 紧急指针(Urgent Ptr) | 138-153位 | 仅URG=1时有效 | 通常为0 |
TCP校验和的魔鬼细节: TCP校验和计算比IP复杂,因其包含伪首部(Pseudo Header):
伪首部 = 源IP(4字节) + 目的IP(4字节) + 0(1字节) + 协议号(1字节) + TCP总长(2字节)计算步骤:
- 构造伪首部(12字节);
- 将伪首部、TCP首部、TCP载荷(若有)拼接成连续内存块;
- 按16位分组求和,溢出位回卷;
- 对结果取反,填入TCP首部校验和字段。
实操教训:某次编写SYN-ACK包时,因忘记在伪首部中填入TCP总长(应为20字节),导致校验和错误。目标主机返回RST包,Wireshark显示“TCP Checksum Incorrect”,但反复检查TCP首部无果。最终发现伪首部中htons(20)被误写为htons(0)——这种错误在代码里藏得极深,必须用十六进制编辑器逐字节比对伪首部结构。
3.4 UDP首部与“飞鸽欺骗”的轻量级实现逻辑
相比TCP,UDP首部仅8字节,结构简洁,但“飞鸽欺骗”的有效性正源于此:
| 字段 | 长度 | 说明 | 欺骗利用点 |
|---|---|---|---|
| 源端口 | 2字节 | 发送方端口号,可伪造 | 伪装成合法服务端口(如53/DNS) |
| 目的端口 | 2字节 | 接收方端口号 | 广播到通用端口(如2425) |
| 长度 | 2字节 | UDP首部+载荷总长 | 必须准确,否则接收方解析失败 |
| 校验和 | 2字节 | 可选,0表示禁用 | 欺骗时设0,省去计算开销 |
为何UDP欺骗比TCP更易实现?
- 无状态依赖:UDP无需维护连接状态,发完即走,不存在序列号同步、重传机制等复杂逻辑;
- 校验和可禁用:RFC 768允许UDP校验和为0,表示不校验,极大降低构造难度;
- 广播友好:UDP支持255.255.255.255广播,而TCP因面向连接特性无法广播。
我们实现的“飞鸽欺骗”核心逻辑:
- 构造UDP载荷:
b"HELLO|VERSION=2.3|USER=王五|MAC=AA:BB:CC:DD:EE:FF"; - 计算UDP长度:8(UDP头)+ len(载荷);
- 设置源端口为53(DNS端口),增加欺骗可信度;
- 目的端口设为2425(飞鸽默认端口);
- 校验和字段填0;
- 将UDP包封装进IP包,目的IP设为255.255.255.255,TTL=1;
- 最终封装进以太网帧,目的MAC为FF:FF:FF:FF:FF:FF。
实测中,该包发出后3秒内,局域网内所有运行飞鸽的主机均弹出“王五上线”提示——尽管王五根本不存在。这证明欺骗成功,且因UDP无连接特性,整个过程耗时不足10ms,远快于TCP握手的RTT。
4. sendto()系统调用的深层机制:如何让自定义数据包真正飞出去
4.1 sendto()在原始套接字中的特殊语义:从“发送数据”到“提交帧”
普通UDP/TCP socket的sendto()只是将应用层数据交给内核,由内核填充协议头并发送。但在原始套接字中,sendto()的语义彻底改变:它等价于“将你构造好的完整网络层数据包(含IP头)提交给内核的链路层输出队列”。这意味着:
- 你传入
sendto()的buf参数,必须是一个完整的、字节序已转换的IP数据报(含IP首部+传输层首部+载荷); - 内核不会修改你提供的IP首部字段(如TTL、ID、DF),但会自动填充链路层头(MAC地址);
- 若你使用
AF_PACKET(Linux特有),则sendto()可提交完整以太网帧(含MAC头),此时内核连IP层都不介入。
关键参数解析:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);sockfd:原始套接字描述符,创建时指定IPPROTO_TCP或IPPROTO_UDP;buf:指向你构造好的IP数据报起始地址的指针;len:IP数据报总长度(含IP头),不是应用层载荷长度;dest_addr:struct sockaddr_in*,其中sin_addr字段必须与IP首部中的目的IP一致,否则内核拒绝发送(Linux 4.10+强制校验);addrlen:sizeof(struct sockaddr_in)。
注意:若
dest_addr中IP地址与IP首部目的IP不一致,Linux内核会返回EINVAL错误。这是内核的安全保护,防止应用层欺骗路由。
4.2 绕过内核路由表:直接指定输出网卡的两种方式
原始套接字默认走内核路由表,但“飞鸽欺骗”和“三次握手探测”往往需要强制指定从某张网卡发出,例如:
- 主机有eth0(192.168.1.100)和wlan0(10.0.0.100)两张网卡,你想只向192.168.1.0/24子网发包;
- 避免包被路由到错误网关。
方式一:绑定套接字到特定网卡(推荐)
struct sockaddr_in local_addr; local_addr.sin_family = AF_INET; local_addr.sin_port = htons(0); // 任意端口 local_addr.sin_addr.s_addr = inet_addr("192.168.1.100"); // 绑定到eth0 bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr));绑定后,所有sendto()调用均从该IP所属网卡发出。
方式二:使用IP_HDRINCL选项+AF_INET套接字
int one = 1; setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one));此选项告诉内核:“我提供的buf已含IP首部,请勿再添加”。此时sendto()的dest_addr参数仍需填写,但内核仅用其确定输出网卡,不再修改IP头。
实操对比:某次在双网卡服务器上测试,未绑定网卡时,sendto()随机选择eth0或wlan0发包,导致一半探测包发向错误子网。绑定后,100%流量稳定从eth0发出,扫描成功率从48%提升至99.7%。
4.3 sendto()失败的五大原因与现场诊断法
sendto()返回-1并不罕见,以下是高频原因及快速定位法:
| 错误码 | 常见原因 | 诊断命令 | 解决方案 |
|---|---|---|---|
EACCES | 权限不足(非root) | whoami && id | 使用sudo或setcap cap_net_raw+ep /path/to/binary |
EINVAL | IP首部长度错误/目的IP不匹配 | hexdump -C packet.bin | head -20 | 检查IP头tot_len字段,确认dest_addr.sin_addr与IP头一致 |
ENETUNREACH | 目的网络不可达 | ip route get 192.168.1.200 | 检查网卡IP、子网掩码、路由表 |
EMSGSIZE | 包长度超过MTU | cat /sys/class/net/eth0/mtu | 分片或减小载荷(如UDP载荷≤1472字节) |
EHOSTUNREACH | ARP失败(目的MAC未知) | arp -a | grep 192.168.1.200 | 先发ARP请求,或用arping -I eth0 192.168.1.200 |
现场诊断黄金组合:
strace -e trace=sendto,socket,bind,connect ./your_program:跟踪系统调用参数与返回值;tcpdump -i eth0 -w debug.pcap port 2425 or port 80:捕获实际发出的包;- `Wireshark打开debug.pcap,右键“Protocol Preferences → TCP → Validate checksums”关闭**:避免校验和错误干扰分析。
我们曾因EMSGSIZE错误卡住2天,最终发现是Wireshark显示的“Packet size”包含以太网帧头,而sendto()的len参数只需IP数据报长度——这个细节在man page里藏得很深。
5. 三次握手连接器的完整实现:从SYN探测到ACK确认的全流程
5.1 连接器设计目标与状态机精简
标准TCP状态机有11种状态,但“三次握手连接器”只需实现三个原子操作:
- SYN探测:发送SYN包,等待SYN-ACK或RST;
- SYN-ACK解析:提取源端口、序列号、确认号、窗口大小、MSS选项;
- ACK确认:构造ACK包,序列号=SYN包ISN+1,确认号=SYN-ACK的序列号+1。
其状态流转为:IDLE → SENDING_SYN → WAITING_SYNACK → SENDING_ACK → CONNECTED。无超时重传、无拥塞控制、无数据传输,纯粹为验证连接可达性。
5.2 SYN包构造与发送:手算校验和的完整代码
以下为Linux C语言实现的SYN包发送核心(已脱敏,可直接编译):
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <netinet/tcp.h> #include <arpa/inet.h> #include <sys/ioctl.h> #include <net/if.h> // IP首部校验和计算 unsigned short csum(unsigned short *buf, int nwords) { unsigned long sum = 0; for (int i = 0; i < nwords; i++) { sum += buf[i]; } sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); return (unsigned short)(~sum); } // TCP校验和计算(含伪首部) unsigned short tcp_csum(struct iphdr *iph, struct tcphdr *tcph) { struct pseudo_header { unsigned int src_addr; unsigned int dst_addr; unsigned char placeholder; unsigned char protocol; unsigned short tcp_length; } psh; psh.src_addr = iph->saddr; psh.dst_addr = iph->daddr; psh.placeholder = 0; psh.protocol = IPPROTO_TCP; psh.tcp_length = htons(sizeof(struct tcphdr)); // 无载荷,仅TCP头 int psize = sizeof(struct pseudo_header); unsigned short *p = malloc(psize + sizeof(struct tcphdr)); memcpy(p, &psh, psize); memcpy((char*)p + psize, tcph, sizeof(struct tcphdr)); unsigned short result = csum(p, (psize + sizeof(struct tcphdr)) / 2); free(p); return result; } int main(int argc, char *argv[]) { if (argc != 3) { fprintf(stderr, "Usage: %s <dst_ip> <dst_port>\n", argv[0]); exit(1); } int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); if (sockfd < 0) { perror("socket"); exit(1); } // 构造IP首部 char packet[1500]; struct iphdr *iph = (struct iphdr*)packet; iph->ihl = 5; iph->version = 4; iph->tos = 0; iph->tot_len = htons(40); // IP头20 + TCP头20 iph->id = htons(54321); iph->frag_off = htons(0x4000); // DF=1 iph->ttl = 64; iph->protocol = IPPROTO_TCP; iph->check = 0; // 先置0 iph->saddr = inet_addr("192.168.1.100"); // 本机IP iph->daddr = inet_addr(argv[1]); // 目的IP // 计算IP校验和 iph->check = csum((unsigned short*)packet, 20); // 构造TCP首部 struct tcphdr *tcph = (struct tcphdr*)(packet + sizeof(struct iphdr)); tcph->source = htons(12345); tcph->dest = htons(atoi(argv[2])); tcph->seq = htonl(11000); // ISN tcph->ack_seq = 0; tcph->doff = 5; // 20字节TCP头 tcph->fin = 0; tcph->syn = 1; tcph->rst = 0; tcph->psh = 0; tcph->ack = 0; tcph->urg = 0; tcph->window = htons(65535); tcph->check = 0; // 先置0 tcph->urg_ptr = 0; // 计算TCP校验和 tcph->check = tcp_csum(iph, tcph); // 发送 struct sockaddr_in dest; dest.sin_family = AF_INET; dest.sin_port = htons(atoi(argv[2])); dest.sin_addr.s_addr = inet_addr(argv[1]); if (sendto(sockfd, packet, 40, 0, (struct sockaddr*)&dest, sizeof(dest)) < 0) { perror("sendto"); } else { printf("SYN sent to %s:%s\n", argv[1], argv[2]); } close(sockfd); return 0; }编译与运行:
gcc -o syn_sender syn_sender.c sudo ./syn_sender 192.168.1.105 805.3 SYN-ACK响应解析:从原始字节流中提取关键信息
当目标主机收到SYN包,若端口开放,会回复SYN-ACK包。你需要用recvfrom()接收,并解析其TCP首部:
char recv_buf[1500]; struct sockaddr_in src_addr; socklen_t addr_len = sizeof(src_addr); int n = recvfrom(sockfd, recv_buf, sizeof(recv_buf)-1, 0, (struct sockaddr*)&src_addr, &addr_len); if (n < 40) return; // 至少IP头20+TCP头20 struct iphdr *r_iph = (struct iphdr*)recv_buf; struct tcphdr *r_tcph = (struct tcphdr*)(recv_buf + (r_iph->ihl * 4)); // 验证是否为SYN-ACK if (r_tcph->syn == 1 && r_tcph->ack == 1) { uint32_t server_isn = ntohl(r_tcph->seq); // 服务器ISN uint32_t expected_ack = ntohl(r_tcph->ack_seq); // 应为11000+1=11001 uint16_t window = ntohs(r_tcph->window); printf("SYN-ACK received: ISN=%u, Window=%u\n", server_isn, window); }关键解析点:
r_iph->ihl * 4计算IP首部长度(支持可变长IP选项);r_tcph->seq是服务器生成的ISN,用于构造后续ACK的ack_seq;r_tcph->ack_seq是服务器期望的下一个序列号,应为你的ISN+1(11000+1=11001),验证握手完整性。
5.4 ACK确认包构造:完成三次握手的最后一击
收到SYN-ACK后,立即构造ACK包:
// 复用之前packet内存 struct tcphdr *ack_tcph = (struct tcphdr*)(packet + sizeof(struct iphdr)); ack_tcph->source = htons(12345); ack_tcph->dest = htons(80); ack_tcph->seq = htonl(11001); // 你的ISN+1 ack_tcph->ack_seq = htonl(server_isn + 1); // 服务器ISN+1 ack_tcph