在前一篇中我们聊了基础的网络连接,但实际网络通信中,除了要传输的数据本身,还会附带大量元数据—— 路由器靠这些信息决定数据包的走向,甚至判断是否要拦截它。今天我们就用 Python 来看看 IP 头,搞懂这些元数据。
IP 头里很多字段小于 1 字节(比如 4 位的版本号),必须用位运算才能正确解析。要解析 IP 头部,首先得拿到原始数据包,用 Python 的 socket 可以实现简单的嗅探器:
import socket# 创建原始套接字(需要管理员权限!)s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP)# 绑定到本机网卡 IP(替换成你的 IP)s.bind(("192.168.1.81", 0))# 开启混杂模式,接收所有数据包s.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)# 接收数据包(IP 包最大 65535 字节)packet = s.recvfrom(65535)packet = packet[0] # 只取数据部分,忽略地址print(packet) # 输出原始字节流(内容较长,会截断)
需要说明一下,这段代码需要使用管理员权限运行,最终输出内容是字节流
IP 头部是数据包开头的一串字节,包含源 IP、目的 IP、TTL 等核心信息。基本规格
- 很多字段小于 1 字节(比如 4 位版本号、3 位标志位)
原始字节流很难直接解析,Python 的 struct.unpack 可以按指定格式把二进制数据转成可读的元组。
字节序(大小端)
- 主机字节序:多数 PC 是小端(
sys.byteorder 查看)
先截取数据包前 20 字节(最小 IP 头部),再按格式解包:
import structimport socket# 截取前 20 字节 IP 头部ip_header_bytes = packet[0:20]# 解包格式:!BBHHHBBH4s4s# !: 网络字节序(大端)# B: Version+IHL (8位)# B: Type of Service (8位)# H: Total Length (16位)# H: Identification (16位)# H: Flags+Fragment Offset (16位)# B: TTL (8位)# B: Protocol (8位)# H: Header Checksum (16位)# 4s: 源 IP (32位)# 4s: 目的 IP (32位)ip_header = struct.unpack('!BBHHHBBH4s4s', ip_header_bytes)print(ip_header)# 示例输出:(69, 0, 1309, 30503, 16384, 128, 6, 0, b'\xc0\xa8\x01Q', b'h\xf4*D')
解包后得到的是元组,我们需要逐个解析字段含义:
1. 版本号(Version)+ 头部长度(IHL)
# 第一个元素是 Version+IHL(8位)version_ihl = ip_header[0]# 版本号:前 4 位(右移 4 位)ip_version = version_ihl >> 4print(f"IP 版本:{ip_version}") # 4(IPv4)# 头部长度:后 4 位(与 0xF 按位与)ihl = version_ihl & 0xFprint(f"IP 头部长度:{ihl * 4} 字节") # IHL 单位是 32位,所以 ×4
2. 服务类型(Type of Service)
tos = ip_header[1]print(f"服务类型:{format(tos, '08b')}") # 00000000(默认优先级)
3. 总长度(Total Length)
total_length = ip_header[2]print(f"数据包总长度:{total_length} 字节") # 1309print(f"实际长度验证:{len(packet)} 字节") # 1309(匹配)
4. 标识 + 标志 + 分片偏移
# 标识(分片重组用)identification = ip_header[3]print(f"数据包标识:{identification}") # 30503# 标志 + 分片偏移(16位)flags_frag = ip_header[4]# 标志:前 3 位(右移 13 位)flags = flags_frag >> 13print(f"分片标志:{format(flags, '03b')}") # 010(禁止分片)# 分片偏移:后 13 位frag_offset = flags_frag & 0b0001111111111111print(f"分片偏移:{frag_offset}") # 0(未分片)
5. TTL + 协议 + 校验和
# 生存时间(跳数)ttl = ip_header[5]print(f"TTL:{ttl}") # 128# 上层协议(6=TCP,17=UDP)protocol = ip_header[6]print(f"上层协议:{protocol}") # 6(TCP)# 头部校验和checksum = ip_header[7]print(f"头部校验和:{format(checksum, '016b')}") # 0000000000000000
6. 源 IP + 目的 IP
# 二进制转点分十进制src_ip = socket.inet_ntoa(ip_header[8])dst_ip = socket.inet_ntoa(ip_header[9])print(f"源 IP:{src_ip}") # 192.168.1.81print(f"目的 IP:{dst_ip}") # 104.244.42.68(Twitter 服务器)
结合上面的内容,我们就可以对IP头进行解析分析,当然原始套接字(SOCK_RAW)要获取未解析的数据包,需要管理员权限,且开启混杂模式后会接收所有经过网卡的数据包。
今天就到这里,下次我们再看看TCP、UDP协议