Skip to content

Commit 2e7c97f

Browse files
authored
Pickle - reconstructs the packet from field values, payload and metadata (#4919)
* Pickle - reconstructs the packet from field valies, payload and metadata * Pickle - support extra slots
1 parent 9ae49f8 commit 2e7c97f

File tree

2 files changed

+125
-30
lines changed

2 files changed

+125
-30
lines changed

scapy/packet.py

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,6 @@ def __init__(self,
214214
else:
215215
self.post_transforms = [post_transform]
216216

217-
_PickleType = Tuple[
218-
Union[EDecimal, float],
219-
Optional[Union[EDecimal, float, None]],
220-
Optional[int],
221-
Optional[_GlobInterfaceType],
222-
Optional[int],
223-
Optional[bytes],
224-
]
225-
226217
@property
227218
def comment(self):
228219
# type: () -> Optional[bytes]
@@ -243,28 +234,64 @@ def comment(self, value):
243234
else:
244235
self.comments = None
245236

237+
@classmethod
238+
def _rebuild_pkt(
239+
cls, # type: Type[Packet]
240+
fields, # type: Dict[str, Any]
241+
payload, # type: Optional[Packet]
242+
metadata, # type: Dict[str, Any]
243+
extra_slots={}, # type: Dict[str, Any]
244+
):
245+
# type: (...) -> Packet
246+
"""Helper for unpickling Packet instances via field values."""
247+
# Create the instance using the field values
248+
pkt = cls(**fields)
249+
if payload is not None:
250+
pkt.add_payload(payload)
251+
# Restore metadata
252+
pkt.time = metadata['time']
253+
pkt.sent_time = metadata['sent_time']
254+
pkt.direction = metadata['direction']
255+
pkt.sniffed_on = metadata['sniffed_on']
256+
pkt.wirelen = metadata['wirelen']
257+
pkt.comments = metadata['comments']
258+
# Restore any extra __slots__ defined by subclasses
259+
for attr, value in extra_slots.items():
260+
setattr(pkt, attr, value)
261+
return pkt
262+
246263
def __reduce__(self):
247-
# type: () -> Tuple[Type[Packet], Tuple[bytes], Packet._PickleType]
248-
"""Used by pickling methods"""
249-
return (self.__class__, (self.build(),), (
250-
self.time,
251-
self.sent_time,
252-
self.direction,
253-
self.sniffed_on,
254-
self.wirelen,
255-
self.comment
256-
))
257-
258-
def __setstate__(self, state):
259-
# type: (Packet._PickleType) -> Packet
260-
"""Rebuild state using pickable methods"""
261-
self.time = state[0]
262-
self.sent_time = state[1]
263-
self.direction = state[2]
264-
self.sniffed_on = state[3]
265-
self.wirelen = state[4]
266-
self.comment = state[5]
267-
return self
264+
# type: () -> Tuple[Any, ...]
265+
"""Used by pickling methods.
266+
267+
Reconstructs the packet from field values, payload, and metadata.
268+
"""
269+
# Store field values for unpickling
270+
fields = {}
271+
for f in self.fields_desc:
272+
if f.name in self.fields:
273+
fields[f.name] = self.fields[f.name]
274+
payload = self.payload # type: Optional[Packet]
275+
if isinstance(payload, NoPayload):
276+
payload = None
277+
# Store metadata for unpickling
278+
metadata = {
279+
'time': self.time,
280+
'sent_time': self.sent_time,
281+
'direction': self.direction,
282+
'sniffed_on': self.sniffed_on,
283+
'wirelen': self.wirelen,
284+
'comments': self.comments,
285+
}
286+
# Collect any extra __slots__ defined by subclasses
287+
extra_slots = {}
288+
for attr in type(self).__all_slots__ - set(Packet.__slots__):
289+
if hasattr(self, attr):
290+
extra_slots[attr] = getattr(self, attr)
291+
return (
292+
type(self)._rebuild_pkt,
293+
(fields, payload, metadata, extra_slots),
294+
)
268295

269296
def __deepcopy__(self,
270297
memo, # type: Any

test/regression.uts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,74 @@ c = pickle.loads(b)
616616
assert c[IP].dst == "192.168.0.1"
617617
assert raw(c) == raw(a)
618618

619+
= Pickle preserves field values, payload and metadata
620+
621+
import pickle
622+
623+
p = IP(src='1.2.3.4', dst='5.6.7.8')/TCP(sport=1234, dport=80, flags='S')
624+
p.time = 12345.0
625+
p.sent_time = 12346.0
626+
p.direction = 1
627+
p.sniffed_on = 'eth0'
628+
p.wirelen = 100
629+
p.comment = b'test comment'
630+
p2 = pickle.loads(pickle.dumps(p))
631+
assert p2[IP].src == '1.2.3.4'
632+
assert p2[IP].dst == '5.6.7.8'
633+
assert p2[TCP].sport == 1234
634+
assert p2[TCP].dport == 80
635+
assert p2[TCP].flags == 'S'
636+
assert p2[IP].len is None
637+
assert p2[IP].chksum is None
638+
assert p2[TCP].chksum is None
639+
assert p2.time == 12345.0
640+
assert p2.sent_time == 12346.0
641+
assert p2.direction == 1
642+
assert p2.sniffed_on == 'eth0'
643+
assert p2.wirelen == 100
644+
assert p2.comment == b'test comment'
645+
assert raw(p2) == raw(p)
646+
647+
= Pickle a bare packet without payload
648+
649+
import pickle
650+
651+
p = IP(src='10.0.0.1')
652+
p2 = pickle.loads(pickle.dumps(p))
653+
assert p2.src == '10.0.0.1'
654+
assert raw(p2) == raw(p)
655+
656+
= Pickle preserves custom __slots__ from subclasses
657+
658+
import pickle
659+
import scapy.packet as _pkt_mod
660+
661+
class _PickleTestPacket(Packet):
662+
__slots__ = ["custom_id", "custom_tag"]
663+
name = "PickleTestPacket"
664+
fields_desc = [ByteField("val", 0)]
665+
666+
# Make the class discoverable by pickle
667+
_pkt_mod._PickleTestPacket = _PickleTestPacket
668+
_PickleTestPacket.__module__ = 'scapy.packet'
669+
670+
p = _PickleTestPacket(val=42)
671+
p.custom_id = 0x123
672+
p.custom_tag = "hello"
673+
p2 = pickle.loads(pickle.dumps(p))
674+
assert p2.val == 42
675+
assert p2.custom_id == 0x123
676+
assert p2.custom_tag == "hello"
677+
678+
# Slots not explicitly set are not serialized
679+
p3 = _PickleTestPacket(val=7)
680+
assert not hasattr(p3, 'custom_id')
681+
p4 = pickle.loads(pickle.dumps(p3))
682+
assert p4.val == 7
683+
assert not hasattr(p4, 'custom_id')
684+
685+
del _pkt_mod._PickleTestPacket
686+
619687
= Usage test
620688

621689
from scapy.main import _usage

0 commit comments

Comments
 (0)