# ebpfcat, A Python-based EBPF generator and EtherCAT master
# Copyright (C) 2021 Martin Teichmann <martin.teichmann@xfel.eu>
# Copyright (C) 2026 European XFEL GmbH
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""\
:mod:`!ebpfcat.xdp` --- support for XDP programs
================================================
"""
__all__ = ["XDPExitCode", "XDPFlags", "PacketVar", "XDP"]
import os
from asyncio import DatagramProtocol, Future, get_event_loop
from contextlib import asynccontextmanager, contextmanager
from enum import Enum
from socket import (
AF_NETLINK, NETLINK_ROUTE, SOCK_DGRAM, if_nametoindex, socket)
from struct import pack, unpack_from
from .bpf import ProgType
from .ebpf import EBPF, MemoryDesc
from .util import sub
class XDPExitCode(Enum):
ABORTED = 0
DROP = 1
PASS = 2
TX = 3
REDIRECT = 4
class XDPFlags(Enum):
SKB_MODE = 2
DRV_MODE = 4 # XDP done by the network driver
class PacketArray:
"""access a packet like a Python array"""
def __init__(self, ebpf, no, memory):
self.ebpf = ebpf
self.no = no
self.memory = memory
def __getitem__(self, pos):
return self.memory[self.ebpf.r[self.no] + pos]
def __setitem__(self, pos, value):
self.memory[self.ebpf.r[self.no] + pos] = value
class Packet:
def __init__(self, ebpf, Else, no):
self.ebpf = ebpf
self.Else = Else
self.no = no
self.pB = PacketArray(self.ebpf, self.no, self.ebpf.mB)
self.pH = PacketArray(self.ebpf, self.no, self.ebpf.mH)
self.pI = PacketArray(self.ebpf, self.no, self.ebpf.mI)
self.pQ = PacketArray(self.ebpf, self.no, self.ebpf.mQ)
class PacketSize:
def __init__(self, ebpf):
self.ebpf = ebpf
@contextmanager
def __lt__(self, value):
e = self.ebpf
e.r9 = e.mA[e.r1]
with e.mA[e.r1 + 4] < e.mA[e.r1] + value as Else:
yield Packet(e, Else, 9)
@contextmanager
def __gt__(self, value):
e = self.ebpf
e.r9 = e.mA[e.r1]
with e.mA[e.r1 + 4] > e.mA[e.r1] + value as Else:
yield Packet(e, Else, 9)
def __le__(self, value):
return self < value + 1
def __ge__(self, value):
return self > value - 1
[docs]
class PacketVar(MemoryDesc):
"""descriptor to access packet data from an XDP program
Declare packet variables as such::
class Program(XDP):
etherType = PacketVar(12, "!H")
:param address: the start address within the packet
:param fmt: the data type of the variable, following the
conventions from the :mod:`struct` module.
"""
base_register = 9
def __init__(self, address, fmt):
self.address = address
self.fmt = fmt
def fmt_addr(self, instance):
return self.fmt, self.address
[docs]
class XDP(EBPF):
"""the base class for XDP programs
XDP programs inherit from this class and define a :meth:`program`
which contains the actual EBPF program. In the class body, variables
are declared using :class:`~ebpfcat.ebpf.LocalVar`, :class:`PacketVar` and
:class:`~ebpfcat.arraymap.ArrayMap`.
.. attribute:: minimumPacketSize
set this to an integer value to declare the minimum size of
a packet. You will only be able to access that many bytes in
the packet. If you need something dynamic, use :attr:`packetSize`
instead.
.. attribute:: defaultExitCode
The default exit code should the packet be smaller than
``minimumPacketSize``. Defaults to ``XDPExitCode.PASS``.
.. attribute:: packetSize
compare this value to a number in your program to allow at
least that many bytes being read. As an example, to assure
at least 20 bytes may be read one would write::
with self.packetSize > 20:
pass
"""
minimumPacketSize = None
defaultExitCode = XDPExitCode.PASS
ebpf_log_level = 0
def __init__(self, **kwargs):
super().__init__(prog_type=ProgType.XDP, **kwargs)
self.packetSize = PacketSize(self)
def program(self):
if self.minimumPacketSize is None:
sub(XDP, self).program()
else:
with self.packetSize > self.minimumPacketSize as packet:
self.pB = packet.pB
self.pH = packet.pH
self.pI = packet.pI
self.pQ = packet.pQ
sub(XDP, self).program()
self.exit(self.defaultExitCode)
def _netlink(self, ifindex, fd, flags):
"""use netlink to attach or detach programs
This uses a blocking API for talking to the kernel, which seems weird
given we are in an asyncio system. But we are just talking to the
kernel, so there is no risk of waiting. This used to be properly
async, but is not anymore. Look in the git history to find the async
version.
"""
sock = socket(family=AF_NETLINK, type=SOCK_DGRAM, proto=NETLINK_ROUTE)
sock.setsockopt(270, 11, 1)
# this was adopted from xdp1_user.c
p = pack("IHHIIBxHiIiHHHHiHHI",
# NLmsghdr
52, # length of if struct
19, # RTM_SETLINK
5, # REQ | ACK
1, # sequence number
0, # pid
# IFI
0, # AF_UNSPEC
0, # type
ifindex,
0, #flags
0, #change
# NLA
20, # length of field
0x802B, # NLA_F_NESTED | IFLA_XDP
# NLA_XDP
8, # length of field
1, # IFLA_XDP_FD
fd,
8,
3, # IFLA_XDP_FLAGS,
flags.value)
sock.sendto(p, (0, 0))
try:
while True:
data = sock.recv(1000)
pos = 0
while (pos < len(data)):
ln, type, flags, seq, pid = unpack_from("IHHII", data, pos)
if type == 3: # DONE
return 0
elif type == 2: # ERROR
errno, *args = unpack_from("iIHHII", data, pos + 16)
if errno != 0:
raise OSError(errno, os.strerror(-errno))
if flags & 2 == 0: # not a multipart message
return 0
pos += ln
raise RuntimeError("Netlink response not understood")
finally:
sock.close()
[docs]
def attach(self, network, flags=XDPFlags.SKB_MODE):
"""attach this program to a ``network``
:param network: the name of the network interface,
like ``"eth0"``
:param flags: one of the :class:`XDPFlags` """
ifindex = if_nametoindex(network)
self.load(log_level=self.ebpf_log_level)
self._netlink(ifindex, self.file_descriptor, flags)
[docs]
def detach(self, network, flags=XDPFlags.SKB_MODE):
"""detach this program from a ``network``
:param network: the name of the network interface,
like ``"eth0"``
:param flags: one of the :class:`XDPFlags` """
ifindex = if_nametoindex(network)
self._netlink(ifindex, -1, flags)
[docs]
@contextmanager
def run(self, network, flags=XDPFlags.SKB_MODE):
"""attach this program to a ``network`` during context
attach this program to the ``network`` while the context
manager is running, and detach it afterwards.
:param network: the name of the network interface,
like ``"eth0"``
:param flags: one of the :class:`XDPFlags` """
ifindex = if_nametoindex(network)
self.load(log_level=self.ebpf_log_level)
try:
self._netlink(ifindex, self.file_descriptor, flags)
finally:
self.close()
try:
yield
finally:
self._netlink(ifindex, -1, flags)