Layer 2 sockets over a PACKET_MMAP ring buffer - #5083
Draft
polybassa wants to merge 1 commit into
Draft
Conversation
A packet socket can carry its packets through a ring of frames it shares with the kernel, mapped once, so that receiving one is a read out of memory and sending one a write followed by a send() that hands the filled frames over. The kernel only asks for CAP_NET_RAW when a packet socket is created: mapping its rings, binding, receiving and sending do not. A privileged program can therefore set a socket up and pass it over a UNIX socket to an unprivileged Scapy, which sniffs and sends on it as if it had opened it itself. The geometry of the rings travels with the descriptor, as the kernel offers no way to ask a socket about it. The read position of a ring only follows the position the kernel writes at as long as it started with it, which a ring taken over while it was already running does not, so the timestamps of its frames tell where to pick it up. AI-Assisted: yes (Cursor) Co-authored-by: Cursor <cursoragent@cursor.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #5083 +/- ##
==========================================
+ Coverage 80.05% 80.55% +0.49%
==========================================
Files 390 391 +1
Lines 96810 97119 +309
==========================================
+ Hits 77499 78231 +732
+ Misses 19311 18888 -423
🚀 New features to boost your workflow:
|
Contributor
Author
|
@gpotter2 What do you think about this PoC |
Contributor
There was a problem hiding this comment.
Pull request overview
Adds Linux PF_PACKET Layer-2 sockets backed by a PACKET_MMAP (TPACKET_V2) ring buffer, enabling zero-copy-ish receive/transmit and allowing privileged socket creation to be handed off to an unprivileged Scapy process via UNIX fd passing.
Changes:
- Introduces
scapy/arch/linux/ringbuf.pywithRingSpec,L2RingSocket,L2ListenRingSocket, fd handover helpers, and a CLI server mode. - Adds UTscapy coverage for ring geometry validation, rx/tx behavior over veth, wrap-around, sniff/sr1 integration, and UNIX-socket handover (
test/linux_ringbuf.uts). - Enables mypy checking for the new module by listing it in
.config/mypy/mypy_enabled.txt.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
scapy/arch/linux/ringbuf.py |
Implements PACKET_MMAP ring-backed L2 sockets, fd sharing/restore, and a CLI socket-sharing server. |
test/linux_ringbuf.uts |
Adds regression tests for ring geometry, rx/tx behavior, and fd handover scenarios. |
.config/mypy/mypy_enabled.txt |
Adds the new ringbuf module to the mypy-enabled list. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+128
to
+133
| if frame_size <= _TPACKET2_HDRLEN or frame_size % TPACKET_ALIGNMENT: | ||
| raise ValueError( | ||
| "frame_size must be over %d and a multiple of %d" % ( | ||
| _TPACKET2_HDRLEN, TPACKET_ALIGNMENT | ||
| ) | ||
| ) |
Comment on lines
+368
to
+373
| poller = select.poll() | ||
| poller.register(self.ins.fileno(), events) | ||
| timeout = None # type: Optional[float] | ||
| if deadline is not None: | ||
| timeout = max(0.0, deadline - time.monotonic()) * 1000 | ||
| return bool(poller.poll(timeout)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
scapy/arch/linux/ringbuf.py, a Linux packet socket that carries its packetsthrough a ring of frames shared with the kernel (TPACKET_V2) instead of
recvfrom()andsend(). The ring is mapped once, so receiving a packet is aread out of memory, and sending one is a write followed by a single
send()that hands the filled frames over.
L2RingSocket— receive and transmit rings, anL2Socketsubclass, sosniff(opened_socket=...),sr1(),srp()andconf.L2socket = L2RingSocketwork unchanged.
select()on a packet socket already reports the ring, so nochange was needed in the sniffing loop.
L2ListenRingSocket— the listen-only variant, which keeps the outgoingpackets and refuses to send.
RingSpec— the geometry (frame size, block size, block count), validatedhere rather than letting the kernel answer a bad one with a bare
EINVAL.sock.stats()— the packets the kernel put in the ring and the ones it had todrop, which is the counter you want when a capture comes up short.
python -m scapy.arch.linux.ringbuf eth0 /run/scapy.sock --owner scapyuser—the privileged side of the handover below, from the command line.
Nothing else in the tree changes, apart from one line adding the module to
.config/mypy/mypy_enabled.txt.Running Scapy without privileges
The kernel checks
CAP_NET_RAWwhen a packet socket is created. Mapping itsrings, binding it, receiving and sending on it do not. A privileged program can
therefore open the socket and pass the descriptor to an unprivileged Scapy,
which then sniffs and sends on it as if it had opened it itself:
share()sends the descriptor overSCM_RIGHTStogether with the geometry ofthe rings, which has to travel with it because the kernel offers no way to ask a
socket about its rings.
from_fd(fd, rx, tx)is the lower-level entry for adescriptor passed some other way, for instance inherited across an
exec.Two details the handover has to get right:
there holds the socket, so the receiving side never drops that membership on
close. The command line server hands each client a socket of its own and lets
go of it without taking the interface back out of promiscuous mode.
long as it started with it. A ring taken over while it was already running
gives neither, so the timestamps of its frames tell where to pick it up. The
scan only happens when the socket is woken with no frame at its own position,
never on the normal path.
Testing
test/linux_ringbuf.uts, 17 tests, marked~ linux needs_root vethand pickedup by the existing
test/*.utsglob:sniff()andsr1()over a ring socketframe
send()when there is no transmit ringmessage that carries no socket
Beyond the suite, the rootless claim was checked with a child process in a
nested user namespace, holding no capability over the network namespace: it
could not open a packet socket of its own (
PermissionError), yet sniffed andsent through the one it was handed, both directly and through the command line
server.
mypy and flake8 are clean on the new module.
Notes
RingSpec(frame_size=…)raised from the 2048-byte default, and what does not fit is dropped by the
kernel and shows up in
stats().which is why the server defaults to mode 0600 and takes an explicit
--owner.