diff --git a/scapy/arch/bpf/supersocket.py b/scapy/arch/bpf/supersocket.py index d26c1be7d44..8587e6fa6b5 100644 --- a/scapy/arch/bpf/supersocket.py +++ b/scapy/arch/bpf/supersocket.py @@ -390,14 +390,31 @@ def send(self, pkt): self.assigned_interface = iff # Build the frame - if self.guessed_cls == Loopback: - # bpf(4) man page (from macOS, but also for BSD): - # "A packet can be sent out on the network by writing to a bpf - # file descriptor. [...] Currently only writes to Ethernets and - # SLIP links are supported" - # - # Headers are only mentioned for reads, not writes. tuntaposx's tun - # device reports as a "loopback" device, but it does IP. + # + # LINKTYPE_NULL / DLT_NULL (Loopback) is a special case. From the + # bpf(4) man page (from macOS/Darwin, but also for BSD): + # + # "A packet can be sent out on the network by writing to a bpf file + # descriptor. [...] Currently only writes to Ethernets and SLIP links + # are supported." + # + # Headers are only mentioned for reads, not writes, and it has the + # name "NULL" and id=0. + # + # The _correct_ behaviour appears to be that one should add a BSD + # Loopback header to every sent packet. This is needed by FreeBSD's + # if_lo, and Darwin's if_lo & if_utun. + # + # tuntaposx appears to have interpreted "NULL" as "no headers". + # Thankfully its interfaces have a different name (tunX) to Darwin's + # if_utun interfaces (utunX). + # + # There might be other drivers which make the same mistake as + # tuntaposx, but these are typically provided with VPN software, and + # Apple are breaking these kexts in a future version of macOS... so + # the problem will eventually go away. They already don't work on Macs + # with Apple Silicon (M1). + if DARWIN and iff.startswith('tun') and self.guessed_cls == Loopback: frame = raw(pkt) else: frame = raw(self.guessed_cls() / pkt) diff --git a/test/bpf.uts b/test/bpf.uts index 869b4674c33..eea7c51141b 100644 --- a/test/bpf.uts +++ b/test/bpf.uts @@ -145,3 +145,23 @@ s.send(IP(dst="8.8.8.8")/ICMP()) s = L3bpfSocket() s.assigned_interface = conf.loopback_name s.send(IP(dst="8.8.8.8")/ICMP()) + += L3bpfSocket - send and sniff on loopback +~ needs_root + +localhost_ip = conf.ifaces[conf.loopback_name].ips[4][0] + +def cb(): + # Send a ping to the loopback IP. + s = L3bpfSocket(iface=conf.loopback_name) + s.send(IP(dst=localhost_ip)/ICMP(seq=1001)) + +t = AsyncSniffer(iface=conf.loopback_name, started_callback=cb) +t.start() +time.sleep(1) +t.stop() +t.join(timeout=1) + +# We expect to see our packet and kernel's response. +len(t.results.filter(lambda p: ( + IP in p and ICMP in p and (p[IP].src == localhost_ip or p[IP].dst == localhost_ip) and p[ICMP].seq == 1001))) == 2