4. 数据包发送与接收机制
Scapy 的收发体系可以理解为两条正交的轴线:一条是“是否等待响应”,另一条是“工作在网络层还是链路层”。把这两条轴线交叉,就得到本节要讲的六个核心函数。理解这张“函数地图”,比死记参数更重要,因为选错函数往往意味着抓不到回包,或者抓到了却匹配不上。
发送:send 与 sendp 的层次差异
send() 工作在第三层,它把数据包交给内核路由,由内核决定从哪个接口、用哪个源地址发出。因此你构造 IP(dst="192.168.1.10")/ICMP() 后直接 send(pkt) 即可,无需关心网卡。而 sendp() 工作在第二层,它绕过内核路由,直接把帧写到指定接口,所以必须显式给出 iface,并且要自己填好 Ether() 头。典型用法是 sendp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="192.168.1.1"), iface="eth0")。
两者都支持 count(发送次数)、inter(包间隔秒数)、loop(无限循环)、verbose(是否打印摘要)。inter 是控制发包速率最直接的手段,例如 inter=0.2 表示每 200 毫秒发一个,用于模拟慢速扫描或避免触发限速。需要提醒的是,send() 默认不等待任何回包,它只负责“发出去”,返回值是已发送包与失败包的统计,不包含响应内容。
收发一体:sr / sr1 / srp / srp1
当需要拿到响应时,就要用 sr 系列。sr() 发送并接收,返回一个二元组 (answered, unanswered),其中 answered 是“请求-响应”配对列表,unanswered 是超时未收到应答的请求列表。这是做批量探测时最常用的形式,因为你能同时看到“谁回了”和“谁没回”。
sr1() 是 sr() 的简化版,只返回第一个匹配的应答包,适合“一问一答”场景,比如探测单个主机是否存活:
ans = sr1(IP(dst="192.168.1.10")/ICMP(), timeout=2, verbose=0)
if ans is None:
print("无响应或超时")
else:
print(ans.summary())
srp() 与 srp1() 是它们在第二层的对应版本,用于 ARP 扫描、DHCP 交互等链路层协议。例如 ARP 扫描:srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="192.168.1.0/24"), timeout=2, iface="eth0"),返回的 answered 中每个元素是 (请求, 应答) 对,遍历即可提取存活主机 IP 与 MAC。
超时与重试:决定“等多久”和“试几次”
timeout 是等待响应的总时长,单位秒。它直接决定扫描速度与漏报率的平衡:设得太短,慢速主机会被误判为不在线;设得太长,批量扫描耗时成倍增长。经验上局域网 ICMP 探测用 1~2 秒,跨网段可放宽到 3~5 秒。
retry 控制重试次数,默认 3 次。当某个请求在 timeout 内没有应答时,Scapy 会重发,直到达到 retry 次数才归入 unanswered。因此实际最长等待时间约为 timeout × retry。在丢包率较高的无线环境中,适当提高 retry 能显著降低漏报,但代价是耗时增加。另一个关键参数是 multi:设为 True 时,Scapy 会接收所有匹配的响应而不只是第一个,适合一个请求可能收到多个应答的场景(如广播探测)。
timeout=2, retry=2 做快速摸底,对 unanswered 中的目标再用 timeout=5, retry=3 复测,兼顾速度与准确率。异步嗅探与回调:sniff 的工作方式
sniff() 不发送任何数据,它被动监听接口上的流量。核心参数有 iface(监听接口)、count(抓够多少个包就停,0 表示不限)、timeout(监听时长)、filter(BPF 过滤表达式,如 "tcp port 80")、prn(每收到一个包就调用的回调函数)、store(是否把包存入返回列表,大数据量时设为 False 可省内存)。
def handle(pkt):
if pkt.haslayer(TCP) and pkt[TCP].dport == 80:
print(pkt[IP].src, "->", pkt[IP].dst)
sniff(iface="eth0", filter="tcp", prn=handle, store=False, timeout=10)
prn 回调是 sniff 的灵魂:它让嗅探从“先抓完再分析”变成“边抓边处理”,适合实时监控、告警触发和长时间运行。若还需要在抓包结束时做收尾,可用 stop_filter 指定停止条件,例如收到特定标志包就退出。需要注意,sniff 默认是阻塞的,会占住当前线程;在需要同时做其他事情时,应把它放到独立线程中运行,或使用 AsyncSniffer 类,通过 start() 与 stop() 控制生命周期。
选型速查
| 需求 | 推荐函数 | 关键参数 |
|---|---|---|
| 只发不收,L3 | send() | count, inter |
| 只发不收,L2 | sendp() | iface, count, inter |
| 一问一答,L3 | sr1() | timeout, retry |
| 批量收发,L3 | sr() | timeout, retry, multi |
| 一问一答,L2 | srp1() | iface, timeout |
| 批量收发,L2 | srp() | iface, timeout, retry |
| 被动监听 | sniff() | filter, prn, store, timeout |