Repository: secdev/scapy

Issue:
Wrong byte order for loopback pcaps
### Brief description

The pcap file format supports two different link-layer header types for loopback: NULL (0), in which the header is in host byte order, and LOOP (108), in which the header is in network byte order.

When scapy reads loopback pcaps, it reads the header in host byte order whether the header type is 0 or 108. When scapy writes loopback pcaps, it stores 108 as the header type but stores the header in host byte order anyway.

This results in Wireshark and other tools saying "Family: Unknown (33554432)" and not decoding the IP packet inside. The code to read pcaps is broken in the same way, so scapy can read its own malformed pcaps but not correctly-formatted ones of type 108.

### Scapy version

2.4.5rc1.dev259

### Python version

3.6.8

### Operating system

RHEL 7.9

### Additional environment information

_No response_

### How to reproduce

Do `wrpcap('loopbackbug.pcap', [Loopback()/IP()/ICMP()])` in scapy and then `tshark -Vr loopbackbug.pcap` from the shell.

### Actual result

It will print "Family: Unknown (33554432)" and not be able to decode the IP or ICMP headers.

### Expected result

It should print "Family: IP (2)" and then decode the IP and ICMP headers.

### Related resources

https://www.tcpdump.org/linktypes.html documents each link header type.


Discussion:
Thanks for reporting the issue. I was able to reproduce it.
@josephcsible Do you have an example pcap where a Loopback type field exists in Wireshark?  As an example I did a ping to 127.0.0.1 and a sniff of lo.

I think Wireshark is doing what it was designed to do in this case.  here is why:
```
In [12]: f=Loopback()/IP()/ICMP()

In [13]: z = Loopback(f.build())

In [14]: z==f
Out[14]: False

In [15]: f
Out[15]: <Loopback  type=IPv4 |<IP  frag=0 proto=icmp |<ICMP  |>>>

In [16]: z
Out[16]: <Loopback  type=IPv4 |<IP  version=4 ihl=5 tos=0x0 len=28 id=1 flags= frag=0 ttl=64 proto=icmp chksum=0x7cde src=127.0.0.1 dst=127.0.0.1 |<ICMP  type=echo-request code=0 chksum=0xf7ff id=0x0 seq=0x0 |>>>

In [17]: f.show()
###[ Loopback ]### 
  type      = IPv4
###[ IP ]### 
     version   = 4
     ihl       = None
     tos       = 0x0
     len       = None
     id        = 1
     flags     = 
     frag      = 0
     ttl       = 64
     proto     = icmp
     chksum    = None
     src       = 127.0.0.1
     dst       = 127.0.0.1
     \options   \
###[ ICMP ]### 
        type      = echo-request
        code      = 0
        chksum    = None
        id        = 0x0
        seq       = 0x0
        unused    = ''
```
You expect the above, but Wireshark has no idea what to do with it.  After all, lo understands TCP... Thus when we sniff:
```
In [7]: pkts[0].layers()
Out[7]: 
[scapy.layers.l2.Ether,
 scapy.layers.inet.IP,
 scapy.layers.inet.ICMP,
 scapy.packet.Raw]
```

Without the upper layers of the onion, Wireshark is confused, as it should be.  It needs hints and I do not believe it has this ability.
> Do you have an example pcap where a Loopback type field exists in Wireshark?

Do a capture on lo with Wireshark. Verify the header type is either NULL or LOOP. Verify that Wireshark can decode the resulting .pcap correctly. Go into scapy and rdpcap that file, then immediately wrpcap the result into a new file. Try to open the new file with Wireshark.
Wireshark sniff results in the outer layer of Ether, as I mentioned above.  Good test idea.  Tcpdump also confirms.
```
In [1]: from scapy.all import *
pkts
In [2]: pkts = rdpcap('wireshark_icmp_lo.pcap')

In [3]: pkts[0]
Out[3]: <Ether  dst=00:00:00:00:00:00 src=00:00:00:00:00:00 type=IPv4 |<IP  version=4 ihl=5 tos=0x0 len=84 id=11719 flags=DF frag=0 ttl=64 proto=icmp chksum=0xee0 src=127.0.0.1 dst=127.0.0.1 |<ICMP  type=echo-request code=0 chksum=0x9675 id=0x2 seq=0x1 unused='' |<Raw  load='i\\xfd\\x8ac\x00\x00\x00\x00\\xaeS\x00\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567' |>>>>
```
Without an Ether() type layer from Scapy and perhaps IP, etc...  Wireshark will not understand what you're trying to do, not without a change to Wireshark source code.

I would say this is Expected Scapy Behavior @guedou 
What are you trying to achieve from a code perspective @josephcsible ?
How could this be considered expected behavior? It's unambiguously violating the PCAP spec.
You're giving Wireshark one layer to decipher.  Wireshark doesn't to my knowledge know how to do such a thing.  Had you properly encapsulated your frame or packet, then Wireshark would act properly.  I wasn't even aware of the Loopback layer until this issue.  So, I tested.  As expected, Ether() is needed.

If you can cite a known pcap that shows a Loopback layer in Wireshark then you may be able to achieve what you want.  Without an example to go by, you're just blaming Scapy for what is Expected Wireshark Behavior.
[loopbackbug2.zip](https://github.com/secdev/scapy/files/10147750/loopbackbug2.zip)
When I'm wrong I'm wrong.

Thank you for providing the pcap @josephcsible.  This helps people to debug where otherwise guesses have to be made.  I tried to come up with a workaround for you but could not.

Good find!
I've never modified scapy with respect to how it dissects but I would guess this would be a good place in the source:
https://github.com/secdev/scapy/blob/master/scapy/layers/l2.py#L661 - 676.

Grep may be a pal here too:
```
/site-packages/scapy$ grep -RHn Loopback 2>/dev/null
layers/lltd.py:402:            24: "softwareLoopback",
layers/inet6.py:50:from scapy.layers.l2 import CookedLinux, Ether, GRE, Loopback, SNAP
layers/inet6.py:4074:bind_layers(Loopback, IPv6, type=socket.AF_INET6)

layers/inet.py:27:    Loopback
layers/inet.py:1027:bind_bottom_up(Loopback, IP, type=0)
layers/inet.py:1028:bind_layers(Loopback, IP, type=socket.AF_INET)
layers/l2.py:618:class Loopback(Packet):
layers/l2.py:621:    name = "Loopback"
layers/l2.py:672:conf.l2types.register(DLT_LOOP, Loopback)
layers/l2.py:673:conf.l2types.register_num2layer(DLT_NULL, Loopback)
contrib/automotive/bmw/definitions.py:4853:UDS_RC.routineControlIdentifiers[0x0303] = "DiagLoopbackStart"
contrib/homeplugav.py:77:                0xA048: "'Loopback Request'",
contrib/homeplugav.py:78:                0xA049: "'Loopback Request Confirmation'",
contrib/homeplugav.py:334:class LoopbackRequest(Packet):
contrib/homeplugav.py:335:    name = "LoopbackRequest"
contrib/homeplugav.py:342:class LoopbackConfirmation(Packet):
contrib/homeplugav.py:343:    name = "LoopbackConfirmation"
contrib/homeplugav.py:1435:bind_layers(HomePlugAV, LoopbackRequest, HPtype=0xA048)
contrib/homeplugav.py:1436:bind_layers(HomePlugAV, LoopbackConfirmation, HPtype=0xA049)
arch/libpcap.py:222:                conf.loopback_name = conf.loopback_name = "Npcap Loopback Adapter"  # noqa: E501
arch/libpcap.py:451:            # Makes send detects when it should add Loopback(), Dot11... instead of Ether()  # noqa: E501
arch/bpf/supersocket.py:27:from scapy.layers.l2 import Loopback
arch/bpf/supersocket.py:393:        if self.guessed_cls == Loopback:
arch/windows/structures.py:545:                ("Loopback", BOOLEAN),
arch/windows/__init__.py:54:NPCAP_LOOPBACK_NAME = r"\Device\NPF_Loopback"
arch/windows/__init__.py:60:            conf.loopback_name = "Microsoft KM-TEST Loopback Adapter"
arch/windows/__init__.py:62:            conf.loopback_name = "Microsoft Loopback Adapter"
arch/windows/__init__.py:64:        conf.loopback_name = "Microsoft Loopback Adapter"
arch/windows/__init__.py:105:    LoopbackAdapter, LoopbackSupport, NdisImPlatformBindingOptions, VlanSupport
arch/windows/__init__.py:568:            # Detect Loopback interface
arch/windows/__init__.py:569:            if "Loopback" in i['name']:
```
I will continue the investigations. This fixes the issue `wrpcap('loopbackbug.pcap', [Loopback()/IP()/ICMP()], linktype=DLT_NULL)`

Resolving patch:
diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py
--- a/scapy/layers/l2.py
+++ b/scapy/layers/l2.py
@@ -20,8 +20,7 @@
 from scapy import consts
 from scapy.data import ARPHDR_ETHER, ARPHDR_LOOPBACK, ARPHDR_METRICOM, \
     DLT_ETHERNET_MPACKET, DLT_LINUX_IRDA, DLT_LINUX_SLL, DLT_LINUX_SLL2, \
-    DLT_LOOP, DLT_NULL, ETHER_ANY, ETHER_BROADCAST, ETHER_TYPES, ETH_P_ARP, \
-    ETH_P_MACSEC
+    DLT_LOOP, DLT_NULL, ETHER_ANY, ETHER_BROADCAST, ETHER_TYPES, ETH_P_ARP, ETH_P_MACSEC
 from scapy.error import warning, ScapyNoDstMacException, log_runtime
 from scapy.fields import (
     BCDFloatField,
@@ -689,15 +688,27 @@ def i2m(self, pkt, x):
                   0x18: "IPv6", 0x1c: "IPv6", 0x1e: "IPv6"}
 
 
-class Loopback(Packet):
-    r"""\*BSD loopback layer"""
+# On OpenBSD, Loopback = LoopbackOpenBSD. On other platforms, the 2 are available.
+# This is to be compatible with both tcpdump and tshark
 
+class Loopback(Packet):
+    r"""
+    \*BSD loopback layer
+    """
+    __slots__ = ["_defrag_pos"]
     name = "Loopback"
     if consts.OPENBSD:
         fields_desc = [IntEnumField("type", 0x2, LOOPBACK_TYPES)]
     else:
         fields_desc = [LoIntEnumField("type", 0x2, LOOPBACK_TYPES)]
-    __slots__ = ["_defrag_pos"]
+
+
+if consts.OPENBSD:
+    LoopbackOpenBSD = Loopback
+else:
+    class LoopbackOpenBSD(Loopback):
+        name = "OpenBSD Loopback"
+        fields_desc = [IntEnumField("type", 0x2, LOOPBACK_TYPES)]
 
 
 class Dot1AD(Dot1Q):
@@ -775,8 +786,8 @@ def mysummary(self):
 conf.l2types.register(DLT_LINUX_SLL2, CookedLinuxV2)
 conf.l2types.register(DLT_ETHERNET_MPACKET, MPacketPreamble)
 conf.l2types.register_num2layer(DLT_LINUX_IRDA, CookedLinux)
-conf.l2types.register(DLT_LOOP, Loopback)
-conf.l2types.register_num2layer(DLT_NULL, Loopback)
+conf.l2types.register(DLT_NULL, Loopback)
+conf.l2types.register(DLT_LOOP, LoopbackOpenBSD)
 
 conf.l3types.register(ETH_P_ARP, ARP)
 


Test patch:
diff --git a/test/regression.uts b/test/regression.uts
--- a/test/regression.uts
+++ b/test/regression.uts
@@ -2669,6 +2669,15 @@ with mock.patch("scapy.utils.warning") as warning:
     os.remove(filename)
     assert any("Inconsistent" in arg for arg in warning.call_args[0])
 
+= Check wrpcap() with the Loopback layer
+~ tshark
+
+for cls in [Loopback, LoopbackOpenBSD]:
+    filename = tempfile.mktemp(suffix=".pcap")
+    wrpcap(filename, [cls()/IP()/ICMP()])
+    return_value = b"".join(line for line in tcpdump(filename, prog=conf.prog.tshark, getfd=True))
+    assert b"Echo (ping) request" in return_value
+
 ############
 ############
 + ERF Ethernet format support