Mac环境下如何阻止系统为Python发送的数据包自动添加IP头
Mac下Python发送自定义IP头原始数据包的解决方法
我尝试用Python发送带自定义IP头和ICMP负载的原始IP数据包,运行代码后Wireshark显示系统自动给数据包加了以太网头和IP头。我希望只让系统添加以太网头,IP头完全由自己定义。
原代码如下:
import socket s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW) # IP header ip_header = b'\x45\x00\x00\x1c' # Version, IHL, Type of Service, Total Length ip_header += b'\xab\xcd\x00\x00' # Identification, Flags, Fragment Offset ip_header += b'\x40\x01\x6b\xd8' # TTL, Protocol, Header Checksum ip_header += b'\xc0\xa8\x92\x83' # Source Address ip_header += b'\x08\x08\x08\x08' # Destination Address # IP payload/ICMP header icmp_header = b'\x08\x00\xe5\xca' # Type of message, Code, Checksum icmp_header += b'\x12\x34\x00\x01' # Identifier, Sequence Number packet = ip_header + icmp_header s.sendto(packet, ('8.8.8.8', 1)) s.close()
Wireshark捕获到的数据包情况:
我原本以为设置socket.IP_HDRINCL就能让系统不再自动添加IP头,于是在创建socket后加了这段代码:
s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
但添加后,数据包完全不在Wireshark里显示了。我还试过把协议改成socket.IPPROTO_IP,系统还是会自动添加IP头。
我用的是Mac系统,通过sudo python3 code.py运行代码,想问下怎么让Python/操作系统不再自动给数据包添加IP头?
问题根源
Mac系统的raw socket行为和Linux存在差异:
- 当用
IPPROTO_RAW创建SOCK_RAW socket时,默认内核会自动生成IP头;即便开启IP_HDRINCL,如果自定义IP头的校验和不正确,内核会直接丢弃数据包,导致Wireshark捕获不到。 - Mac内核不会自动修正自定义IP头的校验和,必须手动计算正确值才能让数据包正常发出。
解决步骤
- 开启
IP_HDRINCL选项:明确告诉内核我们将自行提供完整的IP头。 - 手动计算IP头校验和:开启
IP_HDRINCL后内核不再负责校验和计算,错误的校验和会导致数据包被丢弃。 - 确保数据包长度匹配:IP头中的总长度字段必须与实际数据包(IP头+负载)的字节数一致。
修正后的代码
import socket def calculate_ip_checksum(ip_header): # 计算IP头校验和 checksum = 0 # 按16位分组遍历IP头 for i in range(0, len(ip_header), 2): if i + 1 < len(ip_header): word = (ip_header[i] << 8) + ip_header[i+1] checksum += word else: checksum += ip_header[i] << 8 # 合并高16位与低16位 checksum = (checksum >> 16) + (checksum & 0xffff) # 取反得到最终校验和 checksum = ~checksum & 0xffff return checksum.to_bytes(2, byteorder='big') # 创建raw socket s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW) # 开启IP_HDRINCL选项 s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) # 构造IP头(先将校验和字段设为0) ip_header = b'\x45\x00\x00\x1c' # Version(4), IHL(5), Type of Service(0), Total Length(28) ip_header += b'\xab\xcd\x00\x00' # Identification, Flags, Fragment Offset ip_header += b'\x40\x01\x00\x00' # TTL(64), Protocol(ICMP=1), 校验和占位 ip_header += b'\xc0\xa8\x92\x83' # 源IP地址 ip_header += b'\x08\x08\x08\x08' # 目的IP地址 # 计算并替换校验和 ip_header = ip_header[:10] + calculate_ip_checksum(ip_header) + ip_header[12:] # ICMP负载 icmp_header = b'\x08\x00\xe5\xca' # ICMP类型(Echo Request), 代码(0), 校验和 icmp_header += b'\x12\x34\x00\x01' # 标识符, 序列号 packet = ip_header + icmp_header # 发送数据包,地址参数会被IP头中的目的地址覆盖,仅需传占位值 s.sendto(packet, ('0.0.0.0', 0)) s.close()
额外说明
- 以太网头是内核自动添加的,属于正常行为,raw socket无法绕过数据链路层的以太网封装,这符合你“仅添加以太网头,自行提供IP头”的需求。
- 必须用
sudo运行代码,因为创建raw socket需要管理员权限。 - 如果修改了ICMP负载,需要重新计算ICMP的校验和,否则数据包同样可能被丢弃。
内容的提问来源于stack exchange,提问作者Michael M.
相关产品推荐
相关产品推荐

