使用Python原始套接字发送TCP SYN时setsockopt报错问题
解决Python原始套接字发送TCP SYN包的IP_HDRINCL报错问题
咱一步步拆解下你遇到的问题:设置IP_HDRINCL选项后触发[Errno 22] Invalid argument,去掉它虽然能运行但Wireshark抓到两个TCP头,核心原因是原始套接字中IP头的控制权分配问题。
为什么加IP_HDRINCL=1会报错?
当你通过setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)告诉内核“我自己来构造IP头”时,内核会严格校验你发送的数据包——如果你的代码没有构造完整且符合规范的IP头(比如IP版本不对、头长度计算错误、校验和未正确生成、总长度不匹配等),内核就会判定参数无效,抛出Errno 22。
为什么去掉选项后会出现两个TCP头?
不设置IP_HDRINCL时,内核会自动帮你填充IP头,但如果你的代码里已经构造了完整的TCP头,再加上某些系统下原始套接字的默认行为(比如内核误以为你需要额外的TCP头封装),就会出现重复的TCP头。
解决方案
方案1:保留IP_HDRINCL=1,正确构造完整IP+TCP头
如果你想手动控制整个IP包,就必须严格按照IP和TCP协议规范构造每一个字段,尤其是校验和(IP校验和、TCP校验和,TCP校验和还需要包含伪IP头)。这里给你一个简化的构造示例:
import socket import struct def calculate_checksum(data): s = 0 # 按16位分组求和 for i in range(0, len(data), 2): if i + 1 < len(data): w = (data[i] << 8) + data[i+1] else: w = data[i] << 8 s += w # 折叠进位 s = (s >> 16) + (s & 0xffff) s = ~s & 0xffff return s source_ip = "192.168.1.100" dest_ip = "192.168.1.200" dest_port = 27015 # 构造IP头 ip_version_ihl = 0x45 # IPv4 + 5个32位字的头长度 ip_tos = 0 ip_total_len = 0 # 后面会动态计算 ip_id = 12345 ip_flags_frag = 0 ip_ttl = 64 ip_proto = socket.IPPROTO_TCP ip_checksum = 0 ip_src = socket.inet_aton(source_ip) ip_dst = socket.inet_aton(dest_ip) ip_header = struct.pack( '!BBHHHBBH4s4s', ip_version_ihl, ip_tos, ip_total_len, ip_id, ip_flags_frag, ip_ttl, ip_proto, ip_checksum, ip_src, ip_dst ) # 构造TCP SYN包的TCP头 tcp_src_port = 12345 tcp_dst_port = dest_port tcp_seq = 1000 tcp_ack_seq = 0 tcp_data_off_res = 0x50 # 数据偏移(5个32位字)+ 保留位 tcp_flags = 0x02 # SYN标志位 tcp_window = socket.htons(65535) tcp_checksum = 0 tcp_urg_ptr = 0 tcp_header = struct.pack( '!HHLLBBHHH', tcp_src_port, tcp_dst_port, tcp_seq, tcp_ack_seq, tcp_data_off_res, tcp_flags, tcp_window, tcp_checksum, tcp_urg_ptr ) # 构造TCP伪IP头(用于计算TCP校验和) pseudo_header = struct.pack( '!4s4sBBH', ip_src, ip_dst, 0, ip_proto, len(tcp_header) ) tcp_checksum = calculate_checksum(pseudo_header + tcp_header) tcp_header = tcp_header[:16] + struct.pack('!H', tcp_checksum) + tcp_header[18:] # 计算IP总长度并更新IP头 ip_total_len = len(ip_header) + len(tcp_header) ip_header = ip_header[:2] + struct.pack('!H', ip_total_len) + ip_header[4:] # 计算IP校验和并更新IP头 ip_checksum = calculate_checksum(ip_header) ip_header = ip_header[:10] + struct.pack('!H', ip_checksum) + ip_header[12:] # 发送完整数据包 s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP) s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) s.sendto(ip_header + tcp_header, (dest_ip, dest_port))
方案2:不设置IP_HDRINCL,仅构造TCP头
如果你不想手动处理IP头,就只构造TCP SYN的TCP头部分,让内核自动填充IP头。这时候要确保你的代码只发送TCP头,不要包含多余的内容,避免内核重复封装TCP头。示例片段:
import socket import struct def calculate_checksum(data): s = 0 for i in range(0, len(data), 2): if i + 1 < len(data): w = (data[i] << 8) + data[i+1] else: w = data[i] << 8 s += w s = (s >> 16) + (s & 0xffff) s = ~s & 0xffff return s source_ip = "192.168.1.100" dest_ip = "192.168.1.200" dest_port = 27015 # 构造TCP SYN头 tcp_src_port = 12345 tcp_dst_port = dest_port tcp_seq = 1000 tcp_ack_seq = 0 tcp_data_off_res = 0x50 tcp_flags = 0x02 tcp_window = socket.htons(65535) tcp_checksum = 0 tcp_urg_ptr = 0 tcp_header = struct.pack( '!HHLLBBHHH', tcp_src_port, tcp_dst_port, tcp_seq, tcp_ack_seq, tcp_data_off_res, tcp_flags, tcp_window, tcp_checksum, tcp_urg_ptr ) # 构造伪IP头计算TCP校验和 pseudo_header = struct.pack( '!4s4sBBH', socket.inet_aton(source_ip), socket.inet_aton(dest_ip), 0, socket.IPPROTO_TCP, len(tcp_header) ) tcp_checksum = calculate_checksum(pseudo_header + tcp_header) tcp_header = tcp_header[:16] + struct.pack('!H', tcp_checksum) + tcp_header[18:] # 发送TCP头(内核自动加IP头) s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP) # 注释掉IP_HDRINCL设置 # s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) s.sendto(tcp_header, (dest_ip, dest_port))
额外注意事项
- 运行原始套接字代码必须拥有root/管理员权限,否则会出现权限错误。
- 不同操作系统(Linux、Windows、macOS)对原始套接字的处理逻辑有差异,上面的代码可能需要根据你的系统做微调。
内容的提问来源于stack exchange,提问作者Courage
相关产品推荐
相关产品推荐

