diff --git a/ebpfcat/arraymap.py b/ebpfcat/arraymap.py index 288869d6fa32d4d977a3680e08ed7b76b238ef11..226716d4f4ba44f371520740d0eabb44d38a82ac 100644 --- a/ebpfcat/arraymap.py +++ b/ebpfcat/arraymap.py @@ -70,8 +70,9 @@ class ArrayGlobalVarDesc(MemoryDesc): class ArrayMapAccess: """This is the array map proper""" - def __init__(self, data, size): - self.data = data + def __init__(self, fd, size): + self.data = mmap(fd, size) + self.fd = fd self.size = size @@ -99,14 +100,14 @@ class ArrayMap(Map): def __set_name__(self, owner, name): self.name = name - def init(self, ebpf): + def init(self, ebpf, fd): setattr(ebpf, self.name, 0) size = self.collect(ebpf) if not size: # nobody is actually using the map return - fd = create_map(MapType.ARRAY, 4, size, 1, MapFlags.MMAPABLE) - data = mmap(fd, size) - setattr(ebpf, self.name, ArrayMapAccess(data, size)) + if fd is None: + fd = create_map(MapType.ARRAY, 4, size, 1, MapFlags.MMAPABLE) + setattr(ebpf, self.name, ArrayMapAccess(fd, size)) with ebpf.save_registers(list(range(6))), ebpf.get_stack(4) as stack: ebpf.mI[ebpf.r10 + stack] = 0 ebpf.r1 = ebpf.get_fd(fd) diff --git a/ebpfcat/bpf.py b/ebpfcat/bpf.py index ee07c4d42dda2e76c4ac9b7bda713a6b6c8eb2bb..26e11213350b16044c342f43d8c6dffd6adb26b7 100644 --- a/ebpfcat/bpf.py +++ b/ebpfcat/bpf.py @@ -150,6 +150,15 @@ def prog_load(prog_type, insns, license, else: return fd +def obj_pin(pathname, fd): + pn = pathname.encode("utf8") + bpf(6, "QI", addrof(pn), fd) + +def obj_get(pathname): + pn = pathname.encode("utf8") + fd, _ = bpf(7, "Q", addrof(pn)) + return fd + def prog_test_run(fd, data_in, data_out, ctx_in, ctx_out, repeat=1): if isinstance(data_in, int): diff --git a/ebpfcat/ebpf.py b/ebpfcat/ebpf.py index 879759f6bd15312360a94fb0190ad7af1974956f..c53420f1ac42b6d889bbdb8496f7739ae562b62b 100644 --- a/ebpfcat/ebpf.py +++ b/ebpfcat/ebpf.py @@ -1082,6 +1082,9 @@ class MemoryMap: class Map(ABC): """The base class for all maps""" + def __set_name__(self, owner, name): + self.filename = name + @abstractmethod def init(self, ebpf): """create the map and initialize its values""" @@ -1213,15 +1216,25 @@ class TemporaryDesc(RegisterDesc): class EBPF: """The base class for all EBPF programs - This class may even be instantiated directly, in which case you - can just issue the program before the it is loaded. + Usually this class is sub-classed, and the actual program is defined + in the overwritten `program` method. Then the program may be loaded into + the kernel. Alternatively, this class may even be instantiated directly, + in which case you can just issue the program before it is loaded. + + After a program is loaded, its maps may be written to a bpf file system + using `pin_maps`. Those maps may be used at a later time, especially also + in a different task, if the parameter `load_maps` is given, in which case + we assume the program has already been loaded. + + :param load_maps: a prefix to load pinned maps from. Must be existing in a + bpf file system, and usually ends in a "/". """ stack = 0 name = None license = None def __init__(self, prog_type=0, license=None, kern_version=0, - name=None, subprograms=()): + name=None, load_maps=None, subprograms=()): self.opcodes = [] self.prog_type = prog_type if license is not None: @@ -1232,7 +1245,7 @@ class EBPF: self.name = self.__class__.__name__[:16] else: self.name = name - self.loaded = False + self.loaded = load_maps is not None self.mB = MemoryMap(self, "B") self.mH = MemoryMap(self, "H") @@ -1257,9 +1270,23 @@ class EBPF: for p in subprograms: p.ebpf = self - for v in self.__class__.__dict__.values(): + for k, v in self.__class__.__dict__.items(): + if isinstance(v, Map): + if load_maps is None: + v.init(self, None) + else: + v.init(self, bpf.obj_get(load_maps + k)) + + def pin_maps(self, path): + """pin the maps of this program to files with prefix `path` + + This path must be in a bpf file system, and all parent + directories must already exist, while the individual files + must not exist. + """ + for k, v in self.__class__.__dict__.items(): if isinstance(v, Map): - v.init(self) + bpf.obj_pin(path + k, getattr(self, v.name).fd) def program(self): """overwrite this method with your program while subclassing""" diff --git a/ebpfcat/hashmap.py b/ebpfcat/hashmap.py index 126f0c7746d91b00218b0bfa19baf48c2e852428..555ba4434685a50a401bb0b46068429fb0cfc2ed 100644 --- a/ebpfcat/hashmap.py +++ b/ebpfcat/hashmap.py @@ -99,8 +99,9 @@ class HashMap(Map): self.vars.append(ret) return ret - def init(self, ebpf): - fd = create_map(MapType.HASH, 1, 8, self.count) + def init(self, ebpf, fd): + if fd is None: + fd = create_map(MapType.HASH, 1, 8, self.count) for v in self.vars: getattr(ebpf, v.name).fd = fd diff --git a/ebpfcat/xdp.py b/ebpfcat/xdp.py index 7a9a0be06c03a0c18a8fb7e789c688214ea47d01..d38fe93ee4514386c737fbbcb80d225890dbe860 100644 --- a/ebpfcat/xdp.py +++ b/ebpfcat/xdp.py @@ -244,13 +244,13 @@ class XDP(EBPF): await self._netlink(ifindex, fd, flags) async def detach(self, network, flags=XDPFlags.SKB_MODE): - """attach this program from a ``network`` + """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) - await self._netlink(ifindex, -1) + await self._netlink(ifindex, -1, flags) @asynccontextmanager async def run(self, network, flags=XDPFlags.SKB_MODE): diff --git a/examples/ipcounter.py b/examples/ipcounter.py index 0f41aaca71d426ddda5b5caa4d3f6d67f9b447b6..40545192244eac9060317f0c0cfc0dbd56a2e080 100644 --- a/examples/ipcounter.py +++ b/examples/ipcounter.py @@ -1,5 +1,6 @@ """example program to count IPv4 and IPv6 packets""" +from argparse import ArgumentParser from asyncio import get_event_loop, sleep from ebpfcat.arraymap import ArrayMap from ebpfcat.xdp import PacketVar, XDP, XDPExitCode, XDPFlags @@ -22,13 +23,46 @@ class IPCount(XDP): self.exit(XDPExitCode.PASS) +async def show(counter): + for i in range(10): + await sleep(0.1) + print(f"IPv4 {counter.ipv4count} IPv6 {counter.ipv6count}") + + async def main(): - c = IPCount() + parser = ArgumentParser( + prog="ipcount", + description="Count IPv4 and IPv6 packets") + + parser.add_argument("interface", + help="the network interface to listen to") + parser.add_argument("-a", "--attach", action="store_true", + help="attach the bpf program to the interface") + parser.add_argument("-s", "--show", action="store_true", + help="show the number of received packets") + parser.add_argument("-d", "--detach", action="store_true", + help="detach the bpf program from the interface") + args = parser.parse_args() + + if args.attach or not args.show: + c = IPCount() + else: + c = IPCount(load_maps="/sys/fs/bpf/ipcount/") - async with c.run("eth0", XDPFlags.DRV_MODE): - for i in range(10): - await sleep(0.1) - print(f"packets arrived: IPv4 {c.ipv4count} IPv6 {c.ipv6count}") + if args.attach and args.detach: + async with c.run(args.interface, XDPFlags.SKB_MODE): + if args.show: + await show(c) + elif args.attach: + await c.attach(args.interface, XDPFlags.SKB_MODE) + c.pin_maps("/sys/fs/bpf/ipcount/") + if args.show: + await show(c) + else: + if args.show: + await show(c) + if args.detach: + await c.detach(args.interface, XDPFlags.DRV_MODE) if __name__ == "__main__":