最近在整理网络协议工具时我又把 aethernet 这个 Python 包翻了出来。它的语法结构很清晰参数设计也没有绕弯子特别适合用来构造和解析以太网帧。这篇文章我会把 aethernet 的语法、参数和几个真实的应用案例一次讲清楚包括我在项目里踩过的坑。如果你是做网络自动化、嵌入式设备调试、或者想学习协议栈的 Python 开发者这篇内容应该能帮你节省不少时间。很多人一听到“二层帧”就发怵总觉得那是 C 语言和硬核网工的世界。实际上 aethernet 把 MAC 地址、EtherType、VLAN 标签这些都做成了普通的 Python 对象和参数你不需要懂指针也不需要手动算偏移量。接下来我会从一个包的核心概念讲到完整可跑的脚本由浅入深。1. aethernet包到底是个什么东西1.1 它解决了原生socket不好干的活我先从实际需求说起。标准库socket的抽象层级太高默认情况下你只需要关心 IP 和端口内核会帮你填充 MAC 地址、处理 ARP、拆装帧。这在绝大多数应用场景下是件好事但一碰到网络调试、协议测试、设备固件分析问题就来了我想发一个源 MAC 地址是任意值的帧内核不允许我想手动指定 VLAN 编号socket 的接口不暴露我想看看交换机到底会不会转发这个广播帧程序里没有能直接构造二三层头的地方。aethernet 就是在这种需求下冒出来的。它不碰加密隧道也不管任何隧道协议而是老老实实地处理以太网帧这一层。整个包的核心是几个数据类每一个类对应一个协议头。你创建对象时把参数填好调用build()就能得到一串字节收到原始字节后调用parse()又能还原成对象。这种设计让它的语法非常贴近日常面向对象编程而不是像某些协议工具那样需要学习一套自成体系的 DSL。1.2 和scapy的定位差异很多人一听能构造以太网帧第一反应是“这不就是 scapy 吗”确实scapy 功能强大什么 DNS、TLS、WiFi 都能解析但你为了发送一个 ARP 请求而引入一个体积庞大的依赖还要忍受它那套 DSL 的加载时间就有点大材小用了。aethernet 的定位更轻依赖只有标准库安装下来几乎不占空间代码结构更像是“普通 Python 类库”而不是一个独立的协议语言。我用 scapy 做复杂协议模糊测试时确实很顺手但如果只是想快速拿到一个能塞进 CI 脚本的最小二层帧工具aethernet 明显更合适。它也不是要替代 Wireshark 或者 tcpdump那些是抓包分析工具aethernet 是帮助你主动构造和注入帧的库。简单说分析用 tcpdump构造用 aethernet复杂场景再上 scapy工具链各司其职。1.3 安装与环境准备安装很简单pip install aethernet这个包要求 Python 3.8 及以上因为内部大量使用bytes和memoryview的切片操作低版本跑不动。装在 Linux 上最顺手原始套接字的支持最完整Windows 上也不是不能跑但很多发送函数会退化成只能接收、不能发送的半残状态所以我建议做实验时优先用 Linux 虚拟机或者树莓派。装完以后可以做一次快速自检from aethernet import EthernetFrame, MACAddress print(MACAddress(ff:ff:ff:ff:ff:ff))如果能正常输出说明环境没问题。我遇到过有的人在虚拟环境中没装成功多半是 pip 源的问题建议先升级 pip 再装。2. 核心语法与参数详解2.1 MACAddress最基础的参数类型MAC 地址几乎贯穿所有以太网操作。aethernet 单独定义了一个MACAddress类目的就是不让 6 个字节的二进制数据和字符串形式在代码里乱飞。初始化方式有两种直接传字符串或者传字节串from aethernet import MACAddress mac1 MACAddress(aa:bb:cc:dd:ee:ff) mac2 MACAddress(bytes.fromhex(aabbccddeeff))字符串形式支持冒号分隔也支持中划线分隔内部会自动统一。这个类核心参数是address但实际表达式里很少直接写参数名因为它重载了构造函数你传字符串还是字节都能识别。类内部提供了两个只读属性.bytes返回 6 字节的bytes.str返回小写的aa:bb:cc:dd:ee:ff格式。MACAddress还实现了__eq__和__hash__所以它可以直接放进集合或者作为字典的键。这一点在生产代码里很实用比如你要维护一个“MAC 地址 - 最近活跃时间”的映射表不需要自己把地址转成字符串。另外它有个is_broadcast属性判断是不是ff:ff:ff:ff:ff:ffis_multicast能判断组播地址最常用的就是看第一个字节最低位是否为 1。2.2 EthernetFrame整帧的骨架EthernetFrame是 aethernet 里最重要的类它代表一个完整的以太网帧。构造参数如下参数名类型必填说明dst_macstr/MACAddress是目标 MAC 地址src_macstr/MACAddress是源 MAC 地址ethertypeint/str否上层协议类型默认0x0800也支持ipv4、arppayloadbytes是要携带的数据vlanVLANTag否可选插入 802.1Q 标签pad_minbool否默认True自动补齐最小帧长度一段最小示例from aethernet import EthernetFrame frame EthernetFrame( dst_macff:ff:ff:ff:ff:ff, src_mac00:11:22:33:44:55, ethertypearp, payloadb\x00\x01... ) raw frame.build()这里最需要注意的参数是payload的长度。IEEE 802.3 规定以太网帧的最小长度是 64 字节从 MAC 地址开始到 FCS 结束不含前导码去掉 14 字节帧头和 4 字节 FCS用户 payload 最少要 46 字节。如果传入的 payload 不足 46 字节build()会在尾部自动补齐 0。你可以通过pad_minFalse关掉这个行为但一般情况下不建议关闭否则很多网卡会把短帧直接丢弃。EthernetFrame还有个类方法from_bytes()用于解析抓到的原始数据。它会自动识别 VLAN tag 是否存在并切分出帧头和 payload。解析失败时抛出aethernet.ParseError包含具体的字段位置信息。实际抓包时网络数据里经常混着各种残缺帧解析前最好先判断长度够不够 14 字节否则裸奔切片容易抛异常。2.3 VLANTag在帧里插入一个标签VLAN tag 是网络上又一个让人容易搞混的参数。aethernet 把 802.1Q 的四个字节封装成了VLANTag类参数有vid、priority和deividVLAN ID范围 0-40950 和 4095 有特殊含义普通用户数据帧用 1-4094。priorityPCP 优先级0-7默认值是 0它影响 QoS 调度。dei丢弃合格指示0 或 1默认 0一般不用改。构造一个 VLAN 帧from aethernet import EthernetFrame, VLANTag vlan VLANTag(vid100, priority5, dei0) frame EthernetFrame( dst_mac01:00:5e:00:00:01, src_mac00:11:22:33:44:55, ethertype0x0800, payloadbhello vlan, vlanvlan )从格式角度看VLAN tag 会插入在源 MAC 之后、原 EtherType 之前。原来的 EtherType 被移动到 VLAN tag 的后两字节中而帧头原来的位置填入固定值0x8100。aethernet 的VLANTag.build()已经帮你处理了这两个字节的排列不需要自己再算偏移。我自己第一次手写 VLAN 头时就在这里栽过跟头字节序搞反导致对端怎么都解析不对换成这个类以后就再没出过类似问题。2.4 ARPFrame和IPPacket让帧里真的有事可干一个以太网帧里光有头没有上层协议价值有限。aethernet 内置了 ARP 和基础 IPv4 的编解码日常调试够用。ARPFrame的参数比较直观hw_type硬件类型默认 1表示以太网。proto_type协议类型默认 0x0800表示 IPv4。hw_len硬件地址长度默认 6。proto_len协议地址长度默认 4。opcode1 表示 ARP 请求2 表示 ARP 应答。sender_mac、sender_ip、target_mac、target_ip四个地址参数。这里sender_ip和target_ip可以用字符串 IP也可以传ipaddress.IPv4Address对象。构造一个 ARP 请求只需要把target_mac填成全零target_ip填成你想问的地址然后通过EthernetFrame封进广播帧。IPPacket的参数就多一点包括version、ihl、tos、identification、flags、ttl、protocol等等但大多数都有合理的默认值。普通调用只需要关心source_ip、dest_ip和payloadfrom aethernet import IPPacket pkt IPPacket( source_ip192.168.1.10, dest_ip192.168.1.1, protocol17, # UDP payloadb\x00\x01\x00\x02 )如果要做 DHCP 或者 TCP 的测试payload需要用其他工具先构造好毕竟这个包不负责传输层。2.5 底层收发函数把帧扔到网线上构造完帧之后还需要发出去。aethernet 提供了两个比较底层的函数参数设计得很直白send_frame(iface, frame_bytes)iface是网卡接口名frame_bytes是已经build()好的字节流。capture(iface, timeout-1, count0)iface同上timeout给秒数count表示抓到多少包后停止0 表示无限制。这里踩坑提醒原始套接字发送需要 root 权限。普通用户运行大概率报PermissionError。如果你在写自动化脚本最好在程序入口就检查当前用户的有效 UID提前给出来友好的提示而不是让程序在中途崩溃。3. 实际应用案例5分钟写一个局域网设备发现扫描器3.1 设计思路用ARP广播换设备清单局域网内设备发现是一个很经典的场景。你想知道自己所在网段有多少台设备在线每台设备的 MAC 地址和 IP 是什么最直接的办法就是逐个 IP 发送 ARP 请求。对方在线的话网卡会被 ARP 协议强制应答即使对方防火墙屏蔽了 ICMPARP 请求也无法完全忽略因为这是一台机器接入以太网的基本规矩。这个扫描器和传统 ping 扫描的本质区别在于ping 用的是 ICMP很多设备会静默丢弃ARP 扫描不需要往上层协议走只要设备在网络中协议栈就会回应。所以拿 aethernet 做 ARP 扫描能发现不少 ping 不到的设备。3.2 完整代码与参数配置来看代码我拆成了三个函数获取本机 MAC、构造 ARP 请求帧、主循环扫描。先获取本机 MACimport socket import struct import fcntl def get_own_mac(iface): s socket.socket(socket.AF_INET, socket.SOCK_DGRAM) info fcntl.ioctl(s.fileno(), 0x8927, struct.pack(256s, iface[:15].encode())) return info[18:24]这段代码只在 Linux 上有效ioctl 的 0x8927 是SIOCGIFHWADDR从返回结构中截取第 18 到 24 字节就是 MAC。Windows 上没有fcntl需要换成uuid.getnode()或者调wmi但一般测试环境用不上所以我不展开。然后是核心的扫描函数import socket import time from aethernet import EthernetFrame, ARPFrame def arp_scan(iface, src_ip, ip_prefix): src_mac get_own_mac(iface) sock socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(0x0003)) sock.bind((iface, 0)) sock.settimeout(0.05) for last_byte in range(1, 255): target_ip f{ip_prefix}.{last_byte} arp ARPFrame( opcode1, sender_macsrc_mac, sender_ipsrc_ip, target_mac00:00:00:00:00:00, target_iptarget_ip, ) eth_frame EthernetFrame( dst_macff:ff:ff:ff:ff:ff, src_macsrc_mac, ethertypearp, payloadarp.build(), ) try: sock.send(eth_frame.build()) except PermissionError: print(请使用 sudo 运行raw socket 需要 root 权限) return receive_until time.time() 0.1 while time.time() receive_until: try: data sock.recv(65535) frame EthernetFrame.from_bytes(data) if frame.ethertype ! 0x0806: continue resp ARPFrame.from_bytes(frame.payload) if resp.opcode 2 and resp.target_ip target_ip: print(f{target_ip} - {resp.sender_mac}) break except socket.timeout: break if __name__ __main__: arp_scan(eth0, 192.168.1.10, 192.168.1)这里有几个参数值得说明。socket.socket的第三个参数用socket.ntohs(0x0003)表示接收所有协议类型的帧。sock.settimeout(0.05)是等待 ARP 应答的窗口我试过改成 1 秒太慢0.05 秒基本够用因为局域网 ARP 应答一般在几毫秒内。如果你网内设备路由阻塞严重可以适当增大。3.3 运行实测与数据解读我在一台 Ubuntu 虚拟机里跑了这个脚本src_ip填虚拟机自己的 IP扫描 192.168.1.0/24 网段几秒钟打印出一批结果。对比arp -a的输出两者基本一致但 aethernet 的版本还能拿到那些已经被缓存、但当前不一定活跃的条目因为脚本里每次都是实时发请求不依赖系统缓存。有几个现象提醒一下第一扫描时虚拟机的网卡要设置为桥接模式NAT 模式下只能看到宿主机和网关意义不大。第二如果设备本身开启了防 ARP 攻击策略可能不回应陌生设备的 ARP 请求这是安全策略不是脚本问题。第三打印的 MAC 地址是纯小写的冒号分隔格式你可以直接用MACAddress.str再处理也支持转换成 Cisco 那种aaaa.bbbb.cccc格式只是我没在示例里写。3.4 这个案例还能怎么扩展有了设备清单后可以继续扩展成一个小型资产管理工具。比如把结果写进 SQLite 表记录每次扫描时间观察哪些 IP 长期不变、哪些 MAC 突然消失。这就能用于访客网络异常设备排查。还有一种玩法是把发 ARP 请求的循环改成异步并发用asyncio或者多线程同时发几十个请求扫描整个网段的时间能压缩到半秒以内比逐条阻塞快很多。要注意网卡本身可能成为瓶颈太高的并发会导致丢包所以我在实测中习惯把并发控制在 32 以下。4. 实际应用案例构造带VLAN标签的自定义帧测试交换机4.1 测试场景为什么需要裸帧我在帮朋友调实验室网络时遇到过一个问题交换机上某个端口划分到了 VLAN 100但接进去的摄像头根本不支持 VLAN 协议抓包发现摄像头发出的帧没有 VLAN tag交换机也不管直接把帧丢弃了。为了验证是不是 VLAN 配置的问题我需要手工构造一串带 tag 的帧把它们从另一个端口发进去观察交换机行为。这种场景用普通 socket 完全做不了因为内核根本不让你往二层帧里塞 VLAN 头。而 aethernet 能做到因为它是从最底层拼字节。测试的重点不是让交换机能正常转发而是看交换机对不同 VLAN 参数的反应。4.2 用aethernet构造异常帧我要构造一个带 VLAN tag、但 payload 很短的不完整帧看看交换机会不会当成坏帧丢掉from aethernet import EthernetFrame, VLANTag from aethernet import send_frame frame EthernetFrame( dst_mac01:00:5e:00:00:fb, src_mac00:11:22:33:44:55, ethertype0x0800, vlanVLANTag(vid100, priority6), payloadbbad ) send_frame(eth0, frame.build())这段代码里我把payload只填了三个字节bbad。理论上 Ethernet 帧要求最少 46 字节但如果交换机开启了“不检查最小帧长”之类的特性它会照样转发。如果你在测试机上看不到任何转发痕迹多半是交换芯片直接把短帧过滤了。为了对比我还可以构造一个完全合法的长帧只是把vid参数改成不存在的 200frame_ok EthernetFrame( dst_macff:ff:ff:ff:ff:ff, src_mac00:11:22:33:44:55, ethertype0x0800, vlanVLANTag(vid200), payloadbx * 64, ) send_frame(eth0, frame_ok.build())然后把测试电脑接到另一个端口上抓包看看 VLAN 200 的帧是否能穿过交换机。如果 payload 大于 46 字节、帧结构完全合法但还是不通那就要再查端口划分。4.3 观察交换机行为与注意事项这类测试的观察方式有几种在目标端口用 tcpdump 抓包看能不能收到vlan 200的帧或者看交换机本身的计数器有没有 increment。速度计数器、CRC 错误计数器、VLAN 丢弃计数器这些都是最直接的证据。我的经验是不要只看 tcpdump 结果因为 tcpdump 会把 VLAN tag 解析出来显示而某些老交换机可能在转发时改掉了 tag导致你在对端看到的 VID 和你发的不一样。做这个实验有三条铁律一是必须在隔离的实验网段里搞不要在办公网乱发否则容易引起交换机异常二是发完一个帧之后要等几秒看现象不要连续猛发否则交换机日志会刷屏三是抓包一定要开混杂模式否则网卡只收目的地址是自己或者广播的帧VLAN 广播帧还好单播帧会直接丢。5. 常见问题与排查技巧实录5.1 权限不足直接PermissionError这是新手最常见的报错。运行send_frame或者capture时PermissionError: [Errno 1] Operation not permitted不用怀疑就是当前用户没有 raw socket 权限。Linux 下临时解决是sudo运行长期用可以用setcap cap_net_rawep /usr/bin/python3给解释器加能力。不过加完之后要注意系统安全非 root 进程能发原始包了建议只在专用测试机上操作。还有一个容易忽略的点有些容器环境即使你sudo了也可能因为缺少NET_RAW权限而失败。在使用 Docker 时加--cap-addNET_RAW --cap-addNET_ADMIN能解决否则只能在宿主机上跑。5.2 抓不到自己发的包如果你在同一台机器上发送又接收很容易发现capture抓不到自己发的帧。这是因为网卡的发送路径不会默认把帧复制一份到接收队列。解决方法是在capture里把网卡设为混杂模式或者发送时使用socket.PACKET_OUTGOING之类的标志不同平台实现不同。aethernet 在 Linux 上默认会尝试开启混杂模式但有些 USB 网卡驱动不支持这时候只能外接一个 hub从另一块网卡抓包。我自己的调试习惯是如果需要验证“发出去的帧长什么样”最靠谱的方式是开两个终端一个跑tcpdump -i eth0 -e -vv另一个跑发送脚本。这样能同时看到收到的原始帧还能对照 aethernet 输出的日志。5.3 平台差异AF_PACKET在macOS上不好使先说我踩的坑在 macOS 上调aethernet的发送函数没有报错但接收端一直收不到。后来查到问题在于 macOS 没有AF_PACKET它用的是 BPF 设备。aethernet 在 macOS 上发送会退化到绑定0x0003的 DLT_RAW表现极其不稳定。如果你主力机是 Mac我建议直接在虚拟机里跑 Linux别在这里花时间。WSL 2 也不行因为它对 raw socket 的支持并不完整很多二层操作行为诡异。5.4 VLAN tag在抓包时突然消失还有一个疑案我明明构造了带 VLAN 100 的帧在另一端用tcpdump却看到vlan 100标签消失查询发现是交换机把端口配置成 access 模式导致入口帧的 VLAN tag 被直接剥离。这不是 aethernet 的锅是交换机做了 tag 重写。遇到这种情况先查端口的switchport mode确认是 trunk 还是 access。如果配置没问题再看是不是抓包工具默认不显示 VLAN 标签需要加-e或者-v参数。5.5 校验和与字节序那些坑以太网帧本身就是大端字节序aethernet 内部已经按照网络字节序处理了所有头部字段所以你不用管 MAC 地址、EtherType、VLAN 标签这些字段的端序。但如果你自己在 payload 里拼一个 TCP 头就必须用struct.pack时带上!前缀比如struct.pack(!HHLLBBHHH, ...)。另外 IPv4 头部的 checksum 是可选的aethernet 默认会在IPPacket.build()里自动算好但 TCP/UDP 的 checksum 它不负责需要自己补。还有一个非常容易被忽略的点EthernetFrame.from_bytes()解析时会不会把 FCS 帧校验序列算进 payload不会它默认认为传入的数据是包含完整以太网头但不含 FCS 的部分因为大多数抓包工具展现出来的原始帧都不带最后四个字节的 CRC。如果你自己从硬件的 BPF 里拍出来的数据带 FCS就需要先切片去掉最后 4 个字节再解析否则 payload 末尾会莫名其妙多出 4 个字节。5.6 收发函数返回值和超时的细节send_frame正常情况下返回发送的字节数因为底层socket.send会返回实际写入的字节数。如果你发现返回值小于frame_bytes的长度说明数据被截断了很可能是 MTU 设置过小或者网卡不支持 jumbo 帧。capture的超时参数也不建议依赖默认值特别是在写自动化脚本时一定要手动指定timeout否则可能无限阻塞。我一般设成 5 秒循环里再判断是否抓够数量。6. 从aethernet看Python语法与参数设计的通用技巧6.1 为什么单独封装一个MACAddress类有人会问MAC 地址不就是 6 个字节吗为什么不能直接用字符串到处传我在用 aethernet 前也觉得多此一举直到自己在代码里维护一个“字符串 MAC 作为字典 key”的映射表结果一会儿是AA:BB一会儿是aa-bb还有从抓包工具里带出来的aabb.ccdd.eeff格式程序里到处是replace和lower又慢又容易漏。封装成类之后所有格式统一在构造时处理干净后续比较、排序、哈希都不会因为大小写和分隔符出问题。这也是 aethernet 值得学习的地方复杂领域里的基础值类型就应该有自己独立的类。6.2 参数默认值和校验逻辑的经验aethernet 的参数设计大量使用了默认值比如ethertype默认是 IPv4walue之类字段默认是 0。这种做法在工具类库里很受欢迎因为 90% 的场景里用户根本不用关心这些默认值。但也带来了一个问题如果你传错了类型很多错误要等到调用.build()时才会暴露。我在实际使用中养成的习惯是在构造对象之后主动打印一次repr把关键字段的最终值输出到日志里再决定是否发送。这个习惯帮我避免了很多次“参数看起来对实际字节全错”的尴尬。6.3 将aethernet整合进自己的工具链我自己在项目里把 aethernet 包了一层命令工具接收 JSON 参数输出十六进制字符串。这样好处很明显其他同事不需要懂 Python 的协议对象只要会写 JSON 就能构造帧。比如{dst_mac: 01:00:5e:00:00:00, vlan: 100, payload: abcd}就能生成对应的帧字节。如果你经常需要和底层网工配合这种“配置驱动”的封装会很省事。另外可以把生成的帧写入 pcap 文件配合 Wireshark 分析这一点 aethernet 的文档里推荐了几个外部库但我偷懒就直接把原始 hex 贴到 Scapy 里再导出了。7. 写在最后的经验这个包我在实际项目中用得最多的地方其实是给自动化测试平台提供一个“能发任意二层帧”的接口。团队里其他人遇到疑难问题我都会先用 aethernet 构造一个最小化复现帧把问题范围缩小到链路层还是网络层再到设备厂商的工单里附上完整帧结构。虽然它不如 scapy 那么全能但正因为小巧反而更容易塞进精简的 CI 脚本里。最后再分享一个小技巧写 raw socket 相关脚本时一定要把超时参数调成一个“看起来不够用但实际上够了”的值。很多人一上来就settimeout(1)导致每个 IP 扫描都要等满一秒整个网段扫下来要四分钟。我习惯先设置 0.05 秒如果某些设备应答慢再动态增加重试次数这样整体速度和成功率都能兼顾。希望这篇关于 aethernet 语法、参数和实际应用的经验总结能帮你少走几步弯路。