diff --git a/ebpfcat/ebpf.rst b/ebpfcat/ebpf.rst index 8184d415aef76e63b2a9e19acd32b69e0a9d8ebc..2dfc5c4264236123f0d830f0d8cd6847e10fb20d 100644 --- a/ebpfcat/ebpf.rst +++ b/ebpfcat/ebpf.rst @@ -20,13 +20,13 @@ incoming packages. We start with declaring the variables that we want to see both in the XDP program and in user space:: - from ebpfcat.hashmap import HashMap + from ebpfcat.arraymap import ArrayMap from ebpfcat.xdp import XDP, XDPExitCode class Count(XDP): license = "GPL" # the Linux kernel wants to know that... - userspace = HashMap() + userspace = ArrayMap() count = userspace.globalVar() # declare a variable in the map Next comes the program that we want to run in the kernel. Note that this @@ -73,28 +73,48 @@ For reference, this is the full example: .. literalinclude:: /examples/count.py -Conditional statements ----------------------- +Maps +---- -During code generation, all code needs to be executed. This means that -we cannot use a Python ``if`` statement, as then the code actually does not -get executed, so no code would be generated. So we replace ``if`` statements -by Python ``with`` statements like so:: +Maps are used to communicate to the outside world. They look like instance +variables. They may be used from within the EBPF program, and once it is +loaded also from everywhere else. There are two flavors: `arraymap.ArrayMap` +and `hashmap.HashMap`. They have different use cases: - with self.some_variable > 6 as Else: - do_someting - with Else: - do_something_else +Array Maps +~~~~~~~~~~ -certainly an ``Else`` statement may be omitted if not needed. +Array maps are share memory between EBPF programs and user space. All programs +as well as user space are accessing the memory at the same time, so concurrent +access may lead to problems. An exception is the in-place addition operator +`+=`, which works under a lock, but only if the variable is of 4 or 8 +bytes size. -No loops --------- +Otherwise variables may be declared in all sizes. The declaration is like so:: -There is no way to declare a loop, simply because EBPF does not allow it. -You may simply write a ``for`` loop in Python as long as everything can -be calculated at generation time, but this just means that the code will show -up in the EPBF as often as the loop is iterated at generation time. + class MyProgram(EBPF): + array_map = ArrayMap() + a_byte_variable = array_map.globalVar("B") + an_integer_variable = array_map.globalVar("i") + +those variables can be accessed both from within the ebpf program, as from +outside. Both sides are actually accessing the same memory, so be aware of +race conditions. + +Hash Maps +~~~~~~~~~ + +all hash map variables have a fixed size of 8 bytes. Accessing them is +rather slow, but is done with proper locking: concurrent access is possible. +When accessing them from user space, they are read from the kernel each time +anew. They are declared as follows:: + + class MyProgram(EBPF): + hash_map = HashMap() + a_variable = hash_map.globalVar() + +They are used as normal variables, like in `self.a_variable = 5`, both +in EBPF and from user space once loaded. Accessing the packet -------------------- @@ -134,7 +154,7 @@ is too small (by default ``XDPExitCode.PASS``). So the above example becomes:: class Program(XDP): minimumPacketSize = 16 - userspace = HashMap() + userspace = ArrayMap() count = userspace.globalVar() def program(self): @@ -149,7 +169,7 @@ example simplifies to:: class Program(XDP): minimumPacketSize = 16 - userspace = HashMap() + userspace = ArrayMap() count = userspace.globalVar() etherType = PacketVar(12, "!H") # use network byte order @@ -157,48 +177,55 @@ example simplifies to:: with self.etherType == 0x800: self.count += 1 +Programming +----------- -Maps ----- +The actual XDP program is a class that inherits from ``XDP``. The class body +contains all variable declarations, and a method ``program`` which is the +program proper. It is executed by Python, and while executing an EPBF program +is created, which can then be loaded into the linux kernel. -Maps are used to communicate to the outside world. They look like instance -variables. They may be used from within the EBPF program, and once it is -loaded also from everywhere else. There are two flavors: `hashmap.HashMap` -and `arraymap.ArrayMap`. They have different use cases: +Expressions +~~~~~~~~~~~ -Hash Maps -~~~~~~~~~ +Once a variable is declared, it can be used very close to normal Python syntax. +Standard arithmetic works, like ``self.distance = self.speed * self.time``, +given that all are declared variables. Note that you cannot use usual Python +variables, as accessing them does not generate any EBPF code. Use local +variables for that. -all hash map variables have a fixed size of 8 bytes. Accessing them is -rather slow, but is done with proper locking: concurrent access is possible. -When accessing them from user space, they are read from the kernel each time -anew. They are declared as follows:: +Local variables +~~~~~~~~~~~~~~~ - class MyProgram(EBPF): - hash_map = HashMap() - a_variable = hash_map.globalVar() +local variables are seen only by one EBPF program, they cannot be seen by +other programs or user space. They are declared in the class body like this:: -They are used as normal variables, like in `self.a_variable = 5`, both -in EBPF and from user space once loaded. + class Program(XDP): + local_variable = LocalVar("I") -Array Maps -~~~~~~~~~~ +Conditional statements +~~~~~~~~~~~~~~~~~~~~~~ + +During code generation, all code needs to be executed. This means that +we cannot use a Python ``if`` statement, as then the code actually does not +get executed, so no code would be generated. So we replace ``if`` statements +by Python ``with`` statements like so:: -from an EBPF program's perspective, all EPBF programs are accessing the same -variables at the same time. So concurrent access may lead to problems. An -exception is the in-place addition operator `+=`, which works under a lock, -but only if the variable is of 4 or 8 bytes size. + with self.some_variable > 6 as Else: + do_someting + with Else: + do_something_else -Otherwise variables may be declared in all sizes. The declaration is like so:: +certainly an ``Else`` statement may be omitted if not needed. - class MyProgram(EBPF): - array_map = ArrayMap() - a_byte_variable = array_map.globalVar("B") - an_integer_variable = array_map.globalVar("i") +No loops +~~~~~~~~ + +There is no way to declare a loop, simply because EBPF does not allow it. +You may simply write a ``for`` loop in Python as long as everything can +be calculated at generation time, but this just means that the code will show +up in the EPBF as often as the loop is iterated at generation time. -those variables can be accessed both from within the ebpf program, as from -outside. Both sides are actually accessing the same memory, so be aware of -race conditions. Fixed-point arithmetic ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/ebpfcat/xdp.py b/ebpfcat/xdp.py index 0d2b518ae20a81f787d9e469c9666095b31797a8..7a9a0be06c03a0c18a8fb7e789c688214ea47d01 100644 --- a/ebpfcat/xdp.py +++ b/ebpfcat/xdp.py @@ -153,6 +153,17 @@ class PacketSize: 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 :module:`struct` module. + """ base_register = 9 def __init__(self, address, fmt): @@ -164,7 +175,34 @@ class PacketVar(MemoryDesc): class XDP(EBPF): - """the base class for XDP programs""" + """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:`ebpf.LocalVar`, :class:`PacketVar` and + :class:`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 :var:`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 @@ -196,22 +234,34 @@ class XDP(EBPF): transport.get_extra_info("socket").close() async def attach(self, network, flags=XDPFlags.SKB_MODE): - """attach this program to a `network`""" + """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) fd, _ = self.load(log_level=1) await self._netlink(ifindex, fd, flags) async def detach(self, network, flags=XDPFlags.SKB_MODE): - """attach this program from a `network`""" + """attach 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) await self._netlink(ifindex, -1) @asynccontextmanager async def run(self, network, flags=XDPFlags.SKB_MODE): - """attach this program to a `network` during context + """attach this program to a ``network`` during context + + attach this program to the ``network`` while the context + manager is running, and detach it afterwards. - 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) fd, _ = self.load(log_level=1) try: