news 2026/10/14 6:33:47

原始套接字实战:手写SYN包与UDP欺骗的底层原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
原始套接字实战:手写SYN包与UDP欺骗的底层原理

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包,向全网段广播“我在线”消息。这要求你:

  1. 能指定任意源IP地址(普通UDP socket受内核限制,源IP只能是本机真实IP);
  2. 能控制UDP包的TTL值(设为1确保不离开本子网);
  3. 能绕过内核对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,因此你必须:
    1. 调用arping命令或发送ARP请求包;
    2. 解析返回的ARP应答包,提取目标MAC;
    3. 将该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,偏移=0iph->frag_off = htons(0x4000)(DF置1)
生存时间(TTL)64-71位每经一跳减1,64/128为常见值局域网探测设TTL=1,广域网设64
协议(Protocol)72-79位TCP=6,UDP=17,ICMP=1iph->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, FINSYN包仅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字节)

计算步骤:

  1. 构造伪首部(12字节);
  2. 将伪首部、TCP首部、TCP载荷(若有)拼接成连续内存块;
  3. 按16位分组求和,溢出位回卷;
  4. 对结果取反,填入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因面向连接特性无法广播。

我们实现的“飞鸽欺骗”核心逻辑:

  1. 构造UDP载荷:b"HELLO|VERSION=2.3|USER=王五|MAC=AA:BB:CC:DD:EE:FF";
  2. 计算UDP长度:8(UDP头)+ len(载荷);
  3. 设置源端口为53(DNS端口),增加欺骗可信度;
  4. 目的端口设为2425(飞鸽默认端口);
  5. 校验和字段填0;
  6. 将UDP包封装进IP包,目的IP设为255.255.255.255,TTL=1;
  7. 最终封装进以太网帧,目的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
EINVALIP首部长度错误/目的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包长度超过MTUcat /sys/class/net/eth0/mtu分片或减小载荷(如UDP载荷≤1472字节)
EHOSTUNREACHARP失败(目的MAC未知)arp -a | grep 192.168.1.200先发ARP请求,或用arping -I eth0 192.168.1.200

现场诊断黄金组合:

  1. strace -e trace=sendto,socket,bind,connect ./your_program:跟踪系统调用参数与返回值;
  2. tcpdump -i eth0 -w debug.pcap port 2425 or port 80:捕获实际发出的包;
  3. `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 80

5.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
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 6:33:46

Node.js+Vue.js构建公益捐赠系统的技术实践

1. 项目概述这个基于Node.js和Vue.js的贫困地区儿童捐赠资助系统&#xff0c;是我去年为一个公益组织开发的实际项目。系统上线后&#xff0c;成功帮助了300多名山区儿童获得了持续的教育资助。不同于普通的电商或众筹平台&#xff0c;这类公益系统在功能设计、数据透明度和用户…

作者头像 李华
网站建设 2026/10/14 6:33:36

入参为接口的线程池抽象复用:Java后端异步任务治理实战

1. 为什么需要“入参为接口”的线程池抽象复用你在生产环境里见过最离谱的线程池写法是什么&#xff1f;我以前维护过一个中等规模的 Java 服务&#xff0c;每个功能模块都自顾自地 new 一个 ThreadPoolExecutor&#xff0c;参数完全是复制粘贴&#xff1a;有的把核心线程数设成…

作者头像 李华
网站建设 2026/10/14 6:33:14

测试工程师如何用好GitHub贡献图:从空白到有方向的职业轨迹

1. 面试时我先看贡献图&#xff0c;再问技术问题在最近一次测试工程师的面试里&#xff0c;我习惯先打开候选人的 GitHub 主页&#xff0c;看一眼贡献图再问技术问题。不是因为那一整片绿色小方块代表全部能力&#xff0c;而是它往往比简历上的“精通”“熟练”诚实得多。GitHu…

作者头像 李华
网站建设 2026/10/14 6:32:44

Java并发编程实战:从synchronized到高性能优化

1. 并发编程的认知误区与现实挑战"加个synchronized就完事儿"这句话在Java开发者中流传甚广&#xff0c;就像给代码贴创可贴一样被滥用。我见过太多生产环境事故&#xff0c;都源于这种简单粗暴的同步策略。去年排查的一个线上死锁案例&#xff0c;就是因为在支付回调…

作者头像 李华
网站建设 2026/10/14 6:32:40

OpenClaw安全部署实战:从沙箱隔离到权限治理的完整指南

简介&#xff1a;开源人工智能智能体OpenClaw因默认权限过高&#xff0c;存在插件投毒、已公开的高危漏洞及提示词注入等风险&#xff0c;360人工智能安全团队于2026年编制了这份安全部署与实践指南&#xff0c;主要面向OpenClaw个人开发者、一人公司、中小企业数字化团队及安全…

作者头像 李华
网站建设 2026/10/14 6:32:26

AI技术日报工作流:分层过滤、结构化生成与反馈闭环

1. 这不是一份“新闻简报”&#xff0c;而是一套可复用的AI内容日更工作流“AI 日报 2026-10-06”——看到这个标题&#xff0c;第一反应不是点开看内容&#xff0c;而是下意识想&#xff1a;谁在发&#xff1f;用什么发&#xff1f;为什么是今天&#xff1f;为什么是这个格式…

作者头像 李华