多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

用Scapy构造SOME/IP报文:车载以太网协议测试的轻量级方案

用Scapy构造SOME/IP报文:车载以太网协议测试的轻量级方案 1. 为什么用Scapy模拟SOME/IP消息搞车载以太网测试这些年我经常遇到一个很尴尬的场景手头没有完整的SOME/IP协议栈也没有昂贵的总线仿真工具但就是要给对端ECU发一条自定义的SOME/IP请求验证它的响应逻辑或容错能力。这时候用Python配合Scapy是最快能落地的方案没有之一。先解释下SOME/IP是什么。SOME/IP全称是Scalable service-Oriented MiddlewarE over IP是车载ECU之间做服务发现和远程调用的一套中间件协议跑在以太网上承载方通常是UDP或TCP。它的核心思路是把ECU提供的功能抽象成服务客户端通过Request/Response或者Fire-and-Forget的方式调用服务端通过提供Service ID和Method ID来区分不同功能。这套协议在整车SOA架构里几乎绕不开智能座舱和自动驾驶域控制器之间的通信大量使用它。Scapy是Python生态里非常老牌的网络报文构造工具传统上大家拿它玩TCP/IP比较多构造个SYN Flood、伪造个DNS响应之类的但用Scapy直接构造应用层协议——尤其是SOME/IP这种带服务发现语义的中间件报文——的资料非常少很多同行还在用最原始的socket拼字节流或者用C拉起一个vsomeip实例来做。这恰恰是大问题。用原生socket发SOME/IP你面对的是几十个字节的裸报文得自己一位一位地填Message ID、Request ID、Length偏移稍不留神就把Length算错对端直接丢弃报文而拉起完整协议栈又太重改一个字段要重新编译根本没有“快速验证”的灵活性。Scapy正好卡在中间它允许你以字段为单位操作报文还能自动做协议层嵌套和长度计算同时保留了底层发送的完全控制权。一句话概括这篇文章要解决的需求当你在测试SOME/IP服务、验证异常场景、构造调试报文时用Scapy在几分钟内拼出想要的SOME/IP消息发到目标IP并且在Wireshark里能看到规规矩矩的协议解析。这个能力对三种人特别有用一是做车载以太网测试的测试工程师二是做SOA服务开发需要自己构造请求的开发三是研究SOME/IP协议栈做规模测试的协议同学。我接下来会从报文结构讲起然后给出可复现的Scapy构造方案再讲发送和抓包验证的完整链路最后把我在实际项目中踩过的坑整理出来。整个过程用到的代码都能直接跑不依赖特殊的硬件和商业工具。2. SOME/IP报文结构拆解构造前必须先搞懂的字段2.1 头部字段与长度计算逻辑SOME/IP的基础报文分为Header和Payload两部分Header固定长度是16字节也就是128比特。这16字节拆成6个字段Message ID4字节高16位是Service ID低16位是Method ID。客户端调用某个服务的方法时靠这两个ID组合来定位。Request ID4字节高16位是Client ID低16位是Session ID。Client ID标识调用方Session ID标识同一Client下的会话序号。Protocol Version1字节当前固定为0x01。Interface Version1字节接口版本号由服务提供方定义通常从0x01开始。Message Type1字节区分请求和响应等类型。Return Code1字节标记调用结果。Length4字节从Request ID字段开始到Payload末尾的总长度。这里最容易被忽视的是Length的计算范围。很多第一次写SOME/IP报文的人会以为Length是整个报文的长度实际上协议规定Length是从Request ID开始的长度也就是第4字节往后。换句话説对于16字节的固定HeaderLength最小也是12——从Request ID到Return Code的这12字节——再加上Payload的长度。如果你手动构造报文时把Length算错一位对端在解析时直接会把头的后半段当成Payload结果就是校验失败、报文被扔。2.2 Message Type与Return Code的取值Message Type主要会用这几个值0x00是REQUEST客户端发起调用0x01是REQUEST_NO_RETURN不需要服务端回应的请求0x02是NOTIFICATION服务端主动推送事件0x80是RESPONSE服务端对REQUEST的应答。后边还有0x81、0x82这类带错误码语义的变体但日常测试最常见的就是0x00、0x01、0x02和0x80。Return Code在REQUEST里一般填0x00在RESPONSE里同样填0x00表示成功。如果需要构造错误响应来测试对端异常处理可以填0x01E_NOT_OK或者0x02E_NOT_REACHABLE这类标准错误码。测试工程师经常故意构造异常Return Code验证客户端在拿到错误响应后是否会走重试或降级逻辑这时候用Scapy构造这种报文非常顺手。2.3 服务发现阶段的特殊报文Magic CookieSOME/IP还有一类非常重要的特殊报文是服务发现消息简称SOME/IP-SD。它复用了SOME/IP的头部但Message ID固定为0xFFFF8100Request ID固定为0x00000000Protocol Version是0x01Interface Version固定为0x01Message Type固定为0x02Return Code固定为0x00。这其实是把头部“僵化”成一个固定前缀后面跟着接下来要说的Magic Cookie格式字段和各个服务条目。SD消息的真正内容是从Payload开始的。第一个需要构造的就是Magic Cookie它是一个固定的16字节串0xFF 0xFF 0xFF 0xFF 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01 0x00。这个字段存在的意义是为了让接收方确认“后面跟着的确实是SOME/IP-SD载荷”做一次格式同步。之后紧跟的是若干服务条目每个条目由Type1字节、Index3字节、Option个数1字节等字段组成用来声明服务实例或订阅服务。SD消息的完整解析是个大工程但如果你只是想模拟一个简单的服务宣告或者查找请求用Scapy把最外层的SOME/IP-SD头构出来已经够用了。2.4 为什么必须理解这些才能用Scapy构造Scapy内置了两个跟SOME/IP有关的层一个叫SOMEIP一个叫SOMEIPSD。SOMEIP是基础报文层对应刚才说到的16字节头部支持字段级填充和自动序列化SOMEIPSD专门用于构造服务发现条目包含Entries和Options的字段。这两个层在功能上覆盖了90%的测试场景。但要注意一个细节Scapy内置的SOMEIP层在构造Length字段时会自动排除Header自己的4个字节。也就是说你只要指定了Request ID、Payload等字段Scapy会把Length算成一个“把后面所有字段加在一起”的值这正好符合SOME/IP的Length定义。很多人在用Scapy构造时还手动去填Length结果发现发出去的报文在Wireshark里解析出来Length是错的就是因为重复计算了。我待会儿实操部分会专门演示这个坑。3. Scapy构造与发送SOME/IP消息的具体实操3.1 环境准备和Scapy安装开始之前先把工具链准备好。Scapy支持Python 3推荐直接用3.8以上的版本。安装Scapy很简单pip install scapy如果你在Linux环境里需要直接操作网卡发包可能还需要装一下libpcap的开发库。Debian/Ubuntu系统可以用命令apt install libpcap-dev装好之后验证一下是否能正常导入SOME/IP层from scapy.contrib.someip import SOMEIP, SOMEIPSD print(SOMEIP layer available)如果导入失败多半是Scapy的contrib模块没有把SOME/IP编译进来。这时候需要确认一下Scapy版本低于2.4.3的老版本对SOME/IP的支持并不完整建议直接升级pip install --upgrade scapy我在Ubuntu 22.04上实测过Scapy 2.5.0和2.6.1都能正常导入SOMEIP层。Windows环境也可以跑但发包时建议配合Npcap驱动纯socket方式在Windows上会受限。3.2 构造一条基础的SOME/IP Request消息来看一段最基础的代码构造一条普通的REQUEST消息并发送到目标ECUfrom scapy.all import IP, UDP, Ether, sendp from scapy.contrib.someip import SOMEIP def build_someip_request(service_id0x1234, method_id0x0001, client_id0x0001, session_id0x0001, payloadb\x01\x02\x03\x04): someip_pkt ( SOMEIP(ServiceIDservice_id, MethodIDmethod_id, ClientIDclient_id, SessionIDsession_id, MsgType0x00, ReturnCode0x00) / payload ) udp_pkt ( IP(dst192.168.1.100) / UDP(sport30500, dport30501) / someip_pkt ) return udp_pkt packet build_someip_request() packet.show()这段代码的核心逻辑是先用SOMEIP层定义头部字段把Payload直接拼在旁边然后再套上UDP和IP头。这里有几个关键参数需要解释ServiceID0x1234和MethodID0x0001组合起来构成Message ID。客户端在调用服务时只有这个组合跟服务端注册的一致请求才会被路由到正确的处理函数。ClientID和SessionID用于标识一次事务。SessionID一般用递增序号但测试时固定填0x0001问题也不大。MsgType填0x00表示这是REQUEST服务端收到后应该回复RESPONSE。UDP的源端口可以任意选一个空闲端口目标端口要根据服务端实际监听端口来填。很多ECU默认监听30490或者30501但实现各有不同建议先用Wireshark抓一下服务正常启动时的服务发现广播看看真正开放的端口。构造完成后可以用packet.show()打印报文结构确认每个字段都落在期望的节拍上。这一步虽然简单但能提前发现问题避免发到对端才被丢包。3.3 构造RESPONSE消息和带业务载荷的消息测试时经常需要模拟服务端给客户端回RESPONSE。跟REQUEST相比RESPONSE的MsgType要填0x80ReturnCode按业务结果填0x00或错误码Message ID必须跟客户端发来的REQUEST保持一致Request IDClientIDSessionID也要原样返回。代码结构跟REQUEST完全一样只是字段值变了def build_someip_response(service_id0x1234, method_id0x0001, client_id0x0001, session_id0x0001, return_code0x00, payloadb\x00\x00\x00\x00): someip_pkt ( SOMEIP(ServiceIDservice_id, MethodIDmethod_id, ClientIDclient_id, SessionIDsession_id, MsgType0x80, ReturnCodereturn_code) / payload ) udp_pkt ( IP(dst192.168.1.200) / UDP(sport30501, dport30500) / someip_pkt ) return udp_pktPayload这里我填的是4字节的\x00\x00\x00\x00实际业务载荷可能是一串序列化后的结构体数据具体格式要看服务的IDL定义。很多SOME/IP服务用vsomeip做序列化常见格式是每个字段按4字节对齐字符串和数组会在开头带长度字段。这块我建议测试前先拿一个正常的服务端日志或者抓包样本对一下Payload字节确认格式后再去构造。3.4 构造SOME/IP-SD服务发现报文如果需要模拟服务上线广播或者服务发现请求就要用到SOMEIPSD层。下面这段代码构造了一个最简单的服务发现广播报文from scapy.all import Ether, IP, UDP, sendp from scapy.contrib.someip import SOMEIP, SOMEIPSD sd_pkt ( Ether(dstff:ff:ff:ff:ff:ff) / IP(src192.168.1.100, dst224.244.224.245) / UDP(sport30490, dport30490) / SOMEIP(ServiceID0xFFFF, MethodID0x8100, ClientID0x0000, SessionID0x0000, MsgType0x02, ReturnCode0x00) / b\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00 / SOMEIPSD(Entries[ # 这里可以填服务条目 ]) ) sendp(sd_pkt, ifaceeth0)这段代码里有几个值得注意的点目标MAC是广播地址SOME/IP-SD的UDP组播地址默认是224.244.224.245端口固定30490这是AUTOSAR规范里定义的保留地址。Message ID固定为0xFFFF8100其中ServiceID0xFFFFMethodID0x8100这就是SD报文的标识。Magic Cookie是我们手动加的那个字节串。Scapy的SOMEIPSD层在构造Entry时会自动处理一部分字段但Magic Cookie位置比较特殊直接作为原始字节拼在后面最稳定。不过有一点我得说实话SOMEIPSD层在处理复杂Entry和Option时的支持并不完善尤其是Option里的IPv4 Endpoint、配置字符串等高级字段Scapy内置的格式可能跟实际ECU的SD实现有细微差别。我的建议是如果只是少量SD报文验证用Scapy足够如果要做完整的SD交互逻辑测试还是得靠vsomeip这类完整协议栈。这里没必要硬撑着用Scapy。3.5 发包方式与参数选择Scapy发送报文有三种常用方式send()走三层需要完整的IP/UDP头适合已经配置好路由的场景。sendp()走二层直接以以太网帧发送需要指定网卡接口和MAC地址适合测试环境里需要精确控制帧头的情况。sr() / sr1()发送并等待响应适合做请求-响应测试可以直接拿到对端的回包。实际测试中我更喜欢用sendp()。因为车载测试环境经常是多网卡、多IP的设备路由表未必按预期走用二层直接指定网卡最靠谱。举个例子sendp(packet, ifaceeth0, count10, inter0.1)这条命令会把包从eth0网卡发出发送10次每次间隔0.1秒。count和inter是发包压力测试常用的参数模拟客户端连续请求场景很方便。如果对端有响应可以用sr1()发一条等一条from scapy.all import sr1 reply sr1(packet, ifaceeth0, timeout2) if reply: reply.show() else: print(timeout, no response)这个方式特别适合你刚搭好一个SOME/IP服务想快速确认它是否响应正确构造的请求。4. 抓包验证与Wireshark解析4.1 怎么确认发出去的报文格式正确构造报文只是第一步发出去之后你有没有真正验证过对端是怎么看待这个报文的这一点很重要因为Scapy的字段序列化过程对你的代码是没有感知的你看到的packet.show()只是它在内存里的结构真正抓到的帧才是对端视角的帧。我习惯的验证方式是在发送端打开Wireshark的抓包窗口再跑发包脚本抓到那一条报文后展开看。如果SOME/IP层能被Wireshark正确解析出来说明头部字段的构造没有问题。顺便看下Length字段是否等于Payload长度12这是个非常重要的快速自检项。Wireshark默认自带SOME/IP解析器支持但有时候版本较老或者没加载插件会直接把SOME/IP的UDP载荷当成普通data显示。这种情况可以下载SOME/IP的Wireshark插件有开源项目专门做这个或者手动用“Decode As”把UDP端口指定为SOME/IP协议。如果你手上是Wireshark 4.0以上的版本官方解析器已经能识别大部分SOME/IP基础报文但SD报文里的Entries和Options解析有时仍会显示为raw data这不影响调试重点还是看Header字段。4.2 字段的自检逻辑抓包后我建议关注这几个关键字段每一项都跟协议规范对应Length应该等于Payload长度12。如果差4说明把Length自身的大小也算进去了这是最典型的Scapy构造错误。Message ID确认Service ID和Method ID落在预期范围。0xFFFF8100是SD专用普通业务报文不应该出现。MsgTypeREQUEST是0x00RESPONSE是0x80NOTIFICATION是0x02搞混了会导致对端完全无法处理。Request ID模拟客户端时Client ID要唯一Session ID每次请求应该递增或至少不重复。有些ECU会检查重复的Session ID并丢弃。我用Wireshark“可读字符串”视图看一个典型REQEUST时实际展开的效果是IP层和UDP层完全正常SOME/IP层各字段清晰可见Payload部分就是那一段业务数据。如果你发现Payload里混进了某些多余的00字节大概率是Scapy自动填充了对齐字段需要去看SOME/IP层的缺省字节对齐策略。4.3 验证Response是否被正确构造构造一个模拟服务端的RESPONSE时抓包验证尤其重要。除了检查头部字段外还要对着客户端发出的REQUEST来核对服务端返回的Message ID是否跟请求一致ClientID和SessionID是否原样带回来了。只要这些一致客户端才能把Response和请求匹配上反之它就会一直傻等超时。我碰到过一种情况测试平台的客户端不检查Return Code只要Message ID和Session ID对得上就当作成功处理。这种情况下你故意构造一个Return Code为0x01的错误响应发给它它居然毫无反应。这说明被测系统对这个字段的校验本身就是弱的这种情况做异常测试时要额外关注不能因为对端不检查就认为协议栈实现正确。5. 常见问题与排查技巧实录5.1 问题快速定位表我用表格整理一下在实际项目中遇到的典型问题方便你对照排查。问题现象可能原因排查方法Wireshark无法识别SOME/IP协议端口未配置为SOME/IP解码用Decode As手动指定协议Length字段显示比预期多4手动计算Length把自身长度算进去了让Scapy自动算Length不手动覆盖对端完全无响应Service ID/Method ID与注册不符用Wireshark抓正常通信报文对比IDSession ID重复导致请求被丢弃每次发包用了相同SessionID递增SessionID重新发送报文能看到但Payload为空构造时SOMEIP层后未拼接Payload检查协议拼接语法是否用/连接广播帧发不出去网卡没有加入组播组或交换机丢弃用本机Wireshark验证是否发送成功单条消息自发自收无法实现仅构造环路地址未走真实网卡改用sendp指定实际网卡接口5.2 典型坑一长度字段算不对这是新手最容易踩的坑也是最容易排查的坑。SOME/IP的Length定义是“从Request ID字段开始到Payload末尾的总字节数”因为Header有16字节其中Message ID和Length本身共8个字节不参与计算所以Length实际等于Payload长度12。Scapy在处理SOMEIP层时默认会把Payload长度加12作为Length字段的值这个行为是内置的你不需要手动去改。如果你为了保险自己手写了一个Length结果就是实际值被覆盖成两个Length相加报文直接错。我的建议是构造阶段不要碰Length字段直接用Scapy默认值然后靠Wireshark的解析结果来校准。遇到了问题再手动调。5.3 典型坑二源端口和目的端口搞反很多ECU在SOME/IP协议栈里做了端口白名单比如固定从30490端口接收SD消息、从30501接收业务消息但也有厂商是动态绑定端口。测试时如果你把源端口和目标端口填反了对端虽然能收到UDP包但应用层过滤时看到端口不匹配直接丢弃。排查方法也很简单先跑一次正常的服务启动流程用Wireshark抓一段参考报文看它的源端口、目标端口以及Message ID然后照着参考报文去改Scapy参数。不要凭印象填端口不同项目的端口规划差别很大。5.4 典型坑三在lo接口发不出来如果你在开发机上直接跑Scapy而且包的目标IP写的是127.0.0.1那么只能走lo接口并且发送时常见问题如下Scapy的sendp发送二层帧到lo时网卡会丢弃因为lo不是标准以太网接口。正确的做法是直接用send()走三层环路或者用UDP socket发送。不过模拟SOME/IP消息的场景一般是跨设备测试很少走loopback所以我更推荐直接用sendp指定实际物理网卡。5.5 典型坑四SD广播帧发不出去构造SD广播报文时如果只是单纯指定组播IP而不指定目标MAC为组播MAC交换机会把帧丢掉。SOME/IP-SD默认目标IP是224.244.224.245对应的组播MAC是01:00:5e:74:f0:f5也就是把IP后23位映射到MAC后23位。用Ether层的时候必须手动设置dst为这个组播MAC否则发出去的对端根本收不到。第3.4节的代码里我已经写好了这个目标MAC实际项目里要注意确认网卡所在的VLAN和交换机是否放通了对应组播组。5.6 构造异常报文时的心得最后分享一个我在做负向测试时的技巧。用Scapy的好处之一是你可以非常方便地构造“畸形”报文——比如故意把Length改小、把ReturnCode改成未知值、把SessionID填成0。这种报文在真实环境中几乎不会出现但恰恰是对端协议栈健壮性最好的试金石。我在测试某个座舱域控制器的SOME/IP服务时就是用Scapy连续发了几个不同异常组合的报文还真发现了一个当请求的Payload长度超过服务端缓冲时直接宕机的问题。如果你想做类似的负向测试建议用Scapy的Raw层绕开SOMEIP层直接构造原始字节流这样可以完全绕过Scapy自动计算Length等机制想怎么改就怎么改from scapy.all import IP, UDP, Raw malformed_pkt ( IP(dst192.168.1.100) / UDP(sport30500, dport30501) / Raw(loadb\x12\x34\x00\x01\x00\x01\x00\x01\x01\x01\x00\x00\xff\xff\xff\xff) ) send(malformed_pkt, ifaceeth0)这片Raw区域的16个字节就是SOME/IP头部你可以按需改动任何一位。这种方式的好处是头部字段完全可控不会因为Scapy自动填充而“纠正”你的错误。6. 结尾这个方案后续还能怎么扩展写到这里我想起一个场景。去年有个项目需要在没有完整SOME/IP协议栈的情况下把一个测试用的假服务端挂在以太网上接受真实客户端的订阅请求并发布模拟数据。我当时就是用Scapy搭了一个“半自动”服务端一个脚本里同时起了两个线程一个线程收包解析请求另一个线程按固定频率用Scapy构造NOTIFICATION消息发给客户端。虽然不及vsomeip那么完整但对验证客户端逻辑已经够用了。这个方案往后还可以扩展。比如配合Scapy的sniff()接口做在线重放测试——把线上抓的pcap文件里的SOME/IP消息读出来改掉关键字段后再发出去用来验证某个字段的敏感性再比如把构造好的报文封装成Pytest测试用例集成到CI流水线里做回归测试前提是对端设备已接入测试网络。这些都是顺手就能做的事不需要额外的框架和工具。我个人在实际操作中的体会是Scapy模拟SOME/IP消息最大的价值不在于它能不能替代完整协议栈而在于它给了你一个能精确控制每一个bit的调试入口。测试车载以太网协议时你真正需要的往往不是“很标准”的报文而是“刚刚好”的报文——能在你需要的字段上做各种变形的那种。Scapy恰好能满足这个需求。
返回列表